/*
 * 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.util.*;
import java.sql.*;
import org.apache.log4j.*;
import com.silverwrist.util.*;
import com.silverwrist.venice.*;
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.security.*;
import com.silverwrist.venice.svc.GlobalSite;
import com.silverwrist.venice.svc.ServiceControl;
import com.silverwrist.venice.util.*;

class UserContextImpl implements UserContext, ServiceProvider, PropertyProvider
{
  /*--------------------------------------------------------------------------------
   * Static data members
   *--------------------------------------------------------------------------------
   */

  // Property set indexes
  static final int PROP_FLAGS = 0;  // flags

  static final BitSet ALL_PROPS;    // BitSet representing all properties

  // Boolean flag indexes
  static final int BF_POSTPICTURES = 0;
  static final int BF_ADM_NOPHOTO = 1;
  static final int BF_MASSMAIL_OPTOUT = 2;

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

  private static final String AUTH_TOKEN_PREFIX = "VQAT:";
  private static final char AUTH_TOKEN_SEP = '|';

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

  private GlobalSite globalsite;           // the global site
  private EnvUser env;                     // the environment store
  private String remote_addr;              // remote address identifier
  private int uid = -1;                    // the user ID we're using
  private int contactid;                   // ID of our contact information
  private int level;                       // base security level for this user
  private int confirm_num;                 // email confirmation number for this user
  private boolean is_anon = false;         // is this the anonymous user?
  private boolean email_verified = false;  // has email address been verified?
  private String username;                 // the user name we're using
  private java.util.Date created;          // when was this user created? (GMT)
  private java.util.Date last_access;      // when did we last log in? (GMT)
  private String m_description;            // personal description
  private java.sql.Date m_dob;             // date of birth
  private String my_email = null;          // my email address (cached)
  private String my_pseud = null;          // my pseud (cached)
  private String full_name = null;         // my full name (cached)
  private Locale my_locale = null;         // my default locale (cached)
  private TimeZone my_tz = null;           // my default timezone (cached)
  private HashMap mru_cache = new HashMap();  // MRU cache for data objects
  private OptionSet flags = null;             // option flags

  /*--------------------------------------------------------------------------------
   * Constructor
   *--------------------------------------------------------------------------------
   */

  UserContextImpl(GlobalSite globalsite, EnvEngine env)
  {
    this.globalsite = globalsite;
    this.env = new EnvUser(env,this);

  } // end constructor

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

  private final void loadUserData(ResultSet rs) throws SQLException
  {
    uid = rs.getInt("uid");
    username = rs.getString("username");
    // skip field "passhash"
    contactid = rs.getInt("contactid");
    is_anon = rs.getBoolean("is_anon");
    email_verified = rs.getBoolean("verify_email");
    // skip fields "lockout", "access_tries"
    confirm_num = rs.getInt("email_confnum");
    level = rs.getInt("base_lvl");
    created = SQLUtil.getFullDateTime(rs,"created");
    last_access = SQLUtil.getFullDateTime(rs,"lastaccess");
    // skip field "passreminder"
    m_description = rs.getString("description");
    m_dob = rs.getDate("dob");

    // purge any "cached" fields that may be left over
    my_email = null;
    my_pseud = null;
    full_name = null;
    my_locale = null;
    my_tz = null;

    if (logger.isDebugEnabled())
    { // spit it all out to debug info
      logger.debug("Loaded: UID " + uid + ", username \"" + username + "\", contactid " + contactid);
      logger.debug("...is_anon " + is_anon + ", email_verified " + email_verified);
      logger.debug("... level " + level);

    } // end if

  } // end loadUserData

  private final void loadPrefs() throws DataException
  {
    Connection conn = null;
    Statement stmt = null;

    try
    { // call through to lower level function
      conn = globalsite.getConnection(null);
      stmt = conn.createStatement();
      ResultSet rs = stmt.executeQuery("SELECT * FROM userprefs WHERE uid = " + uid + ";");
      
      if (!(rs.next()))
	throw new DataException("cannot find preferences for user");

      if (my_tz==null)
	my_tz = TimeZone.getTimeZone(rs.getString("tzid"));
      
      if (my_locale==null)
	my_locale = International.get().createLocale(rs.getString("localeid"));

      // Load the user properties as well.
      rs = stmt.executeQuery("SELECT ndx, data FROM propuser WHERE uid = " + uid + ";");
      while (rs.next())
      { // load the properties...
	switch (rs.getInt(1))
	{ // based on the property, do what is needed
	  case PROP_FLAGS:
	    flags = new OptionSet(rs.getString(2));
	    break;

	  default:
	    break;

	} // end switch

      } // end while

    } // end try
    catch (SQLException e)
    { // translate into DataException, yadda yadda yadda
      logger.error("SQL error reading preferences: " + e.getMessage(),e);
      throw new DataException("Unable to read user preferences: " + e.getMessage(),e);

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

    } // end finally

  } // end loadPrefs

  private final void sendEmailConfirmation() throws DataException, EmailException
  {
    if (logger.isDebugEnabled())
      logger.debug("sendEmailConfirmation(): sending to \"" + my_email + "\"");

    // Create the message to be sent.
    String message = globalsite.getStockMessage("email-confirm");
    if (message==null)
    { // no message defined?  oy!
      logger.error("internal error condition: email-confirm stock message not defined");
      throw new DataException("INTERNAL: email-confirm message not defined");

    } // end if

    // Replace the variables in the text.
    HashMap vars = new HashMap(2);
    vars.put("username",username);
    vars.put("confnum",String.valueOf(confirm_num));
    message = StringUtil.replaceAllVariables(message,vars);

    String subject = globalsite.getStockMessage("email-confirm-subject");
    if (subject==null)
      subject = "Venice Email Confirmation";

    // Create the emailer and send the message.
    MailSend msend = (MailSend)(globalsite.queryService(MailSend.class));
    msend.addRecipient(MailSend.RECIP_TO,my_email);
    msend.setSubject(subject);
    msend.setText(message);
    msend.send();
    if (logger.isDebugEnabled())
      logger.debug("...email sent!");

  } // end sendEmailConfirmation

  private final void autoJoinCommunities(Connection conn) throws SQLException
  {
    if (logger.isDebugEnabled())
      logger.debug("autoJoinCommunities (uid " + uid + ", level " + level + ")");

    PreparedStatement stmt = null, stmt2 = null;

    try
    { // See which communities we are eligible to autojoin.
      stmt = conn.prepareStatement("SELECT sigmember.sigid, sigmember.locked FROM users, sigmember, sigs "
				   + "WHERE sigmember.uid = users.uid AND sigmember.sigid = sigs.sigid "
				   + "AND users.is_anon = 1 AND sigs.join_lvl <= ?;");
      stmt.setInt(1,level);
      ResultSet rs = stmt.executeQuery();

      // Save the community IDs returned into temporary array lists.
      ArrayList tmp_cid = new ArrayList();
      ArrayList tmp_locked = new ArrayList();
      while (rs.next())
      { // save off the "cid" and "locked" column pairs
	tmp_cid.add(new Integer(rs.getInt(1)));
	tmp_locked.add(new Boolean(rs.getBoolean(2)));

      } // end while

      // Figure out which of those communities we haven't joined yet and set up to autojoin them.
      stmt = conn.prepareStatement("SELECT sigid FROM sigmember WHERE sigid = ? AND uid = ?;");
      stmt2 = conn.prepareStatement("INSERT INTO sigmember (sigid, uid, granted_lvl, locked) VALUES (?, ?, ?, ?);");
      SecurityMonitor smon = (SecurityMonitor)(globalsite.queryService(SecurityMonitor.class,"Community"));
      int new_level = smon.getDefaultRole("Community.NewUser").getLevel();
      for (int i=0; i<tmp_cid.size(); i++)
      { // see if the user is already a member of this community
	Integer x_cid = (Integer)(tmp_cid.get(i));
	stmt.setInt(1,x_cid.intValue());
	stmt.setInt(2,uid);
	rs = stmt.executeQuery();
	if (!(rs.next()))
	{ // tack this information onto the end of our big "INSERT" command
	  Boolean x_locked = (Boolean)(tmp_locked.get(i));
	  stmt2.setInt(1,x_cid.intValue());
	  stmt2.setInt(2,uid);
	  stmt2.setInt(3,new_level);
	  stmt2.setInt(4,x_locked.booleanValue() ? 1 : 0);
	  stmt2.executeUpdate();
				 
	} // end if

      } // end for

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

    } // end finally

  } // end autoJoinCommunities

  private final void updateProperties(BitSet delta) throws DataException
  {
    Connection conn = null;
    Statement stmt = null;

    try
    { // get a connection and create a statement
      conn = globalsite.getConnection(null);
      stmt = conn.createStatement();
      StringBuffer sql = new StringBuffer();

      if (delta.get(PROP_FLAGS))
      { // store the flags
	sql.append("UPDATE propuser SET data = '").append(flags.asString()).append("' WHERE uid = ");
	sql.append(uid).append(" AND ndx = ").append(PROP_FLAGS).append(';');
	stmt.executeUpdate(sql.toString());
	sql.setLength(0);

      } // end if

    } // end try
    catch (SQLException e)
    { // database error - this is a DataException
      logger.error("DB error storing user properties: " + e.getMessage(),e);
      throw new DataException("unable to save user properties: " + e.getMessage(),e);

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

    } // end finally

  } // end updateProperties

  private final void updateProperties() throws DataException
  {
    updateProperties(ALL_PROPS);

  } // end updateProperties

  private final UserProperties createProperties()
  {
    UserProperties rc = new UserProperties();
    rc.setDisplayPostPictures(flags.get(BF_POSTPICTURES));
    rc.setMassMailOptOut(flags.get(BF_MASSMAIL_OPTOUT));
    return rc;

  } // end createProperties

  private final BitSet storeProperties(UserProperties props)
  {
    BitSet rc = new BitSet();

    if (flags.assign(BF_POSTPICTURES,props.getDisplayPostPictures()))
      rc.set(PROP_FLAGS);
    if (flags.assign(BF_MASSMAIL_OPTOUT,props.getMassMailOptOut()))
      rc.set(PROP_FLAGS);

    return rc;

  } // end storeProperties

  /*--------------------------------------------------------------------------------
   * Implementations from interface UserContext
   *--------------------------------------------------------------------------------
   */

  public int getUID()
  {
    return uid;

  } // end getUID

  public String getUserName()
  {
    return username;

  } // end getUserName

  public int getContactID()
  {
    return contactid;

  } // end getContactID

  public String getDescription()
  {
    return m_description;

  } // end getDescription

  public boolean isLoggedIn()
  {
    if (logger.isDebugEnabled())
      logger.debug("isLoggedIn(): uid = " + uid + ", is_anon = " + is_anon);
    return ((uid!=-1) && !is_anon);

  } // end is_logged_in

  public boolean isEmailVerified()
  {
    return email_verified;

  } // end isEmailVerified

  public void authenticate(String username, String password) throws AccessError, DataException
  {
    if (isLoggedIn())
    { // already authenticated, can't authenticate again
      logger.error("UserContext already authenticated (with uid " + uid + ")");
      throw new InternalStateError("context already authenticated");

    } // end if

    if (logger.isDebugEnabled())
      logger.debug("authenticate(): authenticating user \"" + username + "\"...");

    Connection conn = null;
    PreparedStatement stmt = null;
    AuditRecord ar = null;
      
    try
    { // look for a user name matching this user record
      conn = globalsite.getConnection(null);
      stmt = conn.prepareStatement("SELECT * FROM users WHERE username = ?;");
      stmt.setString(1,username);
      ResultSet rs = stmt.executeQuery();

      if (!(rs.next()))
      {  // user not found
	logger.error("...user not found");
	ar = new AuditRecord(AuditRecord.LOGIN_FAIL,0,remote_addr,"Bad username: " + username);
	throw new AccessError("The user account you have specified does not exist.  Please try again.");

      } // end if

      int the_uid = rs.getInt("uid");

      if (rs.getBoolean("is_anon"))
      { // can't log in as Anonymous Honyak
	logger.error("...user is the Anonymous Honyak, can't explicitly login");
	ar = new AuditRecord(AuditRecord.LOGIN_FAIL,the_uid,remote_addr,"Anonymous user");
	throw new AccessError("This account cannot be explicitly logged into.  Please try again.");

      } // end if

      if (rs.getBoolean("lockout"))
      { // account locked out
	logger.error("...user is locked out by the Admin");
	ar = new AuditRecord(AuditRecord.LOGIN_FAIL,the_uid,remote_addr,"Account locked out");
	throw new AccessError("This account has been administratively locked out.  Please contact "
			      + "the system administrator for assistance.");

      } // end if

      // hash the password and compare it to the stored password hash
      String hash_value = Generator.hashPassword(password);
      if (!(hash_value.equals(rs.getString("passhash"))))
      { // they specified an incorrect password
	// TODO: advance an "invalid" counter and lock the account out if more than X tries
	logger.warn("...invalid password");
	ar = new AuditRecord(AuditRecord.LOGIN_FAIL,the_uid,remote_addr,"Bad password");
	throw new AccessError("The password you have specified is incorrect.  Please try again.");

      } // end if

      if (logger.isDebugEnabled())
	logger.debug("...authenticated");

      // we're authenticated - load the user data into the context
      loadUserData(rs);
      rs.close();

      // update the "last access" time in the database
      stmt.close();
      stmt = conn.prepareStatement("UPDATE users SET lastaccess = ? WHERE uid = ?;");
      java.util.Date mydate = new java.util.Date();
      SQLUtil.setFullDateTime(stmt,1,mydate);
      stmt.setInt(2,uid);
      stmt.executeUpdate();

      // update the "last access" time in this object
      last_access = mydate;

      // an audit record indicating we logged in OK
      ar = new AuditRecord(AuditRecord.LOGIN_OK,the_uid,remote_addr);

      if (logger.isDebugEnabled())
	logger.debug("...context loaded, we're ready :-)");

    } // end try
    catch (SQLException e)
    { // database error - this is a DataException
      logger.error("DB error reading user data: " + e.getMessage(),e);
      throw new DataException("unable to access user data: " + e.getMessage(),e);

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

    } // end if

  } // end authenticate

  public void confirmEmail(int conf_num) throws AccessError, DataException
  {
    if (logger.isDebugEnabled())
      logger.debug("confirmEmail(): confirming for UID " + uid);
    if ((email_verified) || env.testPermission(EnvUser.PERM_NOEMAILVERIFY))
    { // already confirmed
      if (logger.isDebugEnabled())
	logger.debug("...user has either already confirmed or is exempt");
      return;

    } // end if

    AuditRecord ar = null;

    if (conf_num!=confirm_num)
    { // the confirmation number is wrong
      logger.warn("...confirmation number incorrect");
      ar = new AuditRecord(AuditRecord.VERIFY_FAIL,uid,remote_addr,"Invalid confirmation number");
      ar.store(globalsite);
      throw new AccessError("Confirmation number is incorrect.  Please try again.");

    } // end if

    Connection conn = null;
    PreparedStatement stmt = null;
    SecurityMonitor smon = (SecurityMonitor)(globalsite.queryService(SecurityMonitor.class));
    Role new_role = smon.getDefaultRole("Global.AfterVerify");

    try
    { // get a connection and set the user's status to reflect the verification
      conn = globalsite.getConnection(null);
      stmt = conn.prepareStatement("UPDATE users SET verify_email = 1, base_lvl = ? WHERE uid = ?;");
      stmt.setInt(1,new_role.getLevel());
      stmt.setInt(2,uid);
      stmt.executeUpdate();
      email_verified = true;
      level = new_role.getLevel();

      autoJoinCommunities(conn);  // EJB 4/14/2001 - handle autojoin of any SIGs we couldn't autojoin at
                                  // account creation time

      // record an audit message indicating that we verified OK
      ar = new AuditRecord(AuditRecord.VERIFY_OK,uid,remote_addr);

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

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

    } // end if

  } // end confirmEmail

  public void resendEmailConfirmation() throws DataException, EmailException
  {
    if (logger.isDebugEnabled())
      logger.debug("resendEmailConfirmation(): resending for UID " + uid);
    if ((email_verified) || env.testPermission(EnvUser.PERM_NOEMAILVERIFY))
    { // already confirmed, no need to resend
      if (logger.isDebugEnabled())
	logger.debug("...user has either already confirmed or is exempt");
      return;

    } // end if

    if (my_email==null)
      getContactInfo();  // forces my_email to be updated

    Connection conn = null;
    PreparedStatement stmt = null;
    AuditRecord ar = null;

    try
    { // need to change the user's email confirmation number first
      conn = globalsite.getConnection(null);

      // generate new confirmation number
      int new_confirm_num = Generator.get().getNewConfirmationNumber();

      // create an SQL statement to reset the user account information, and execute it
      stmt = conn.prepareStatement("UPDATE users SET email_confnum = ? WHERE uid = ?;");
      stmt.setInt(1,new_confirm_num);
      stmt.setInt(2,uid);
      stmt.executeUpdate();
      confirm_num = new_confirm_num;  // save changed value

      // now send the email confirmation!
      sendEmailConfirmation();

      // log that a resend was performed...
      ar = new AuditRecord(AuditRecord.RESEND_CONFIRM,uid,remote_addr);

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

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

    } // end if
    
  } // end resendEmailConfirmation

  public ContactInfo getContactInfo() throws DataException
  {
    if (logger.isDebugEnabled())
      logger.debug("getContactInfo() for UID " + uid);

    if (flags==null)
      loadPrefs();
    ContactInfoImpl rc;
    if (contactid>=0)
      rc = new ContactInfoImpl(env,contactid,!(flags.get(BF_ADM_NOPHOTO)));
    else
      rc = new ContactInfoImpl(uid,!(flags.get(BF_ADM_NOPHOTO)));
    if (my_email==null)
      my_email = rc.getEmail();
    if (full_name==null)
    { // compute the full name
      full_name = rc.getGivenName() + " " + rc.getFamilyName();
      full_name = full_name.trim();
      if (StringUtil.isStringEmpty(full_name))
	full_name = username;

    } // end if

    if (my_pseud==null)
      my_pseud = full_name;
    return rc;

  } // end getContactInfo

  public boolean putContactInfo(ContactInfo ci) throws DataException, EmailException
  {
    if (logger.isDebugEnabled())
      logger.debug("putContactInfo() for UID " + uid);

    boolean email_changed = false;
    if ((ci.getOwnerUID()!=uid) || (ci.getOwnerCommunityID()>=0))
    { // the contact information is not owned correctly
      logger.error("ContactInfo ownership wrong (it's " + ci.getOwnerUID() + ", " + ci.getOwnerCommunityID()
		   + "), should be (" + uid + ", -1)");
      throw new DataException("invalid contact information record");

    } // end if

    if (is_anon)
    { // we can't change Anonymous Honyak this way!
      logger.error("cannot modify anonymous user contact info");
      throw new DataException("unmodifiable contact information");

    } // end if

    Connection conn = null;  // database connection
    AuditRecord ar = null;   // audit record
      
    try
    { // get a database connection
      conn = globalsite.getConnection(null);
      Stashable obj = (Stashable)ci;

      // save the contact information
      obj.stash(conn);

      if (contactid<0)
      { // contact being established for the first time
	contactid = ci.getContactID();
	if (logger.isDebugEnabled())
	  logger.debug("...established initial contact (" + contactid + ") for user");
	my_email = ci.getEmail();
	sendEmailConfirmation();
	email_changed = true;

      } // end if

      full_name = ci.getGivenName() + " " + ci.getFamilyName();  // update this field
      my_pseud = full_name;

      if (my_email==null)  // filling in, this is not necessarily the first time
	my_email = ci.getEmail();
      else if (!(my_email.equals(ci.getEmail())) && !(env.testPermission(EnvUser.PERM_NOEMAILVERIFY)))
      { // email address change - need to reconfirm - but choose a new confirmation
	// number and save it in the database first; also turn off the verify flag
	// and set the base level to GLOBAL_UNVERIFIED
	// NOTE: Administrators are exempt from this requirement!

	if (logger.isDebugEnabled())
	  logger.debug("email address changed, need to reconfirm");

	// generate new confirmation number
	int new_confirm_num = Generator.get().getNewConfirmationNumber();
	SecurityMonitor smon = (SecurityMonitor)(globalsite.queryService(SecurityMonitor.class));
	Role new_role = smon.getDefaultRole("Global.Unverified");

	PreparedStatement stmt = null;
	try
	{ // create an SQL statement to reset the user account information, and execute it
	  stmt = conn.prepareStatement("UPDATE users SET verify_email = 0, email_confnum = ?, base_lvl = ? "
				       + "WHERE uid = ?;");
	  stmt.setInt(1,new_confirm_num);
	  stmt.setInt(2,new_role.getLevel());
	  stmt.setInt(3,uid);
	  stmt.executeUpdate();

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

	} // end finally

	// save off changed data
	email_verified = false;
	confirm_num = new_confirm_num;
	level = new_role.getLevel();

	// now send the email confirmation!
	sendEmailConfirmation();

	email_changed = true;

      } // end else if

      ar = new AuditRecord(AuditRecord.USER_CONTACT_INFO,uid,remote_addr,"contactid=" + contactid);

    } // end try
    catch (ClassCastException cce)
    { // we need to be able to coerce the ContactInfo to a Stashable
      logger.error("ContactInfo needs to be a Stashable for this to work");
      throw new DataException("improper contact information record");

    } // end catch
    catch (SQLException e)
    { // database error - this is a DataException
      logger.error("DB error updating contact info: " + e.getMessage(),e);
      throw new DataException("unable to access user contact data: " + e.getMessage(),e);

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

    } // end if

    return email_changed;

  } // end putContactInfo

  public UserProfile getProfile(String xusername) throws DataException
  {
    if (logger.isDebugEnabled())
      logger.debug("getProfile(\"" + xusername + "\")...");
    Connection conn = null;

    try
    { // retrieve a connection from the data pool
      conn = globalsite.getConnection(null);
      UserProfileImpl prof = new UserProfileImpl(env,conn,xusername);
      if (logger.isDebugEnabled())
	logger.debug("...found it!");
      return prof;

    } // end catch
    catch (SQLException e)
    { // turn SQLException into data exception
      logger.error("DB error looking up profile information: " + e.getMessage(),e);
      throw new DataException("Unable to look up user info: " + e.getMessage(),e);

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

    } // end finally

  } // end getProfile

  public UserProfile getProfile(int xuid) throws DataException
  {
    if (logger.isDebugEnabled())
      logger.debug("getProfile(#" + xuid + ")...");
    Connection conn = null;

    try
    { // retrieve a connection from the data pool
      conn = globalsite.getConnection(null);
      UserProfileImpl prof = new UserProfileImpl(env,conn,xuid);
      if (logger.isDebugEnabled())
	logger.debug("...found it!");
      return prof;

    } // end try
    catch (SQLException e)
    { // turn SQLException into data exception
      logger.error("DB error looking up profile information: " + e.getMessage(),e);
      throw new DataException("Unable to look up user info: " + e.getMessage(),e);

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

    } // end finally

  } // end getProfile

  public void setPassword(String password, String reminder) throws DataException
  {
    if (is_anon)
    { // trying to change Anonymous Honyak's (nonexistent) password?
      logger.error("cannot change password of anonymous account");
      throw new DataException("The anonymous account cannot change its password.");

    } // end if

    Connection conn = null;
    PreparedStatement stmt = null;
    AuditRecord ar = null;

    try
    { // retrieve a connection from the data pool
      conn = globalsite.getConnection(null);
      stmt = conn.prepareStatement("UPDATE users SET passhash = ?, passreminder = ? WHERE uid = ?;");
      stmt.setString(1,Generator.hashPassword(password));
      stmt.setString(2,reminder);
      stmt.setInt(3,uid);
      stmt.executeUpdate();

      // record an audit record for this user
      ar = new AuditRecord(AuditRecord.PASSWORD_CHANGE,uid,remote_addr);
      
    } // end try
    catch (SQLException e)
    { // turn SQLException into data exception
      logger.error("DB error changing password: " + e.getMessage(),e);
      throw new DataException("Unable to set user password: " + e.getMessage(),e);

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

    } // end finally

  } // end setPassword

  public void setDescription(String new_descr) throws DataException
  {
    if (is_anon)
    { // trying to change Anonymous Honyak's (nonexistent) description?
      logger.error("cannot change description of anonymous account");
      throw new DataException("The anonymous account cannot change its description.");

    } // end if

    Connection conn = null;
    PreparedStatement stmt = null;

    try
    { // retrieve a connection from the data pool
      conn = globalsite.getConnection(null);
      stmt = conn.prepareStatement("UPDATE users SET description = ? WHERE uid = ?;");
      stmt.setString(1,new_descr);
      stmt.setInt(2,uid);
      stmt.executeUpdate();
      m_description = new_descr;  // change stored information
      
    } // end try
    catch (SQLException e)
    { // turn SQLException into data exception
      logger.error("DB error changing description: " + e.getMessage(),e);
      throw new DataException("Unable to set user description: " + e.getMessage(),e);

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

    } // end finally

  } // end setDescription

  public List getMemberCommunities() throws DataException
  {
    return CommunityUserContextImpl.getMemberCommunityEntries(env);

  } // end getMemberCommunities

  public CommunityContext getCommunityContext(int cid) throws DataException
  {
    return CommunityUserContextImpl.getCommunityContext(env,cid,false);

  } // end getCommunityContext

  public CommunityContext getCommunityContext(String alias) throws DataException
  {
    return CommunityUserContextImpl.getCommunityContext(env,alias,false);

  } // end getCommunityContext

  public List getRootCategoryList() throws DataException
  {
    if (env.getEngine().getParamBoolean(EngineBackend.BP_NOCATEGORIES))
      throw new DataException("The category system has been disabled on this site.");
    return CategoryDescriptorImpl.getTopLevelCategoryList(env,env.testPermission(EnvUser.PERM_SHOWHIDDENCATS));

  } // end getRootCategoryList

  public CategoryDescriptor getCategoryDescriptor(int catid) throws DataException
  {
    if (env.getEngine().getParamBoolean(EngineBackend.BP_NOCATEGORIES))
      throw new DataException("The category system has been disabled on this site.");
    return new CategoryDescriptorImpl(env,catid,!(env.testPermission(EnvUser.PERM_SHOWHIDDENCATS)));

  } // end getCategoryDescriptor

  public List searchForCommunities(int field, int mode, String term, int offset, int count)
      throws DataException
  {
    return CommunityUserContextImpl.searchForCommunities(env,field,mode,term,offset,count);

  } // end searchForCommunities

  public int getSearchCommunityCount(int field, int mode, String term) throws DataException
  {
    return CommunityUserContextImpl.getSearchCommunityCount(env,field,mode,term);

  } // end getSearchCommunityCount

  public List getCommunitiesInCategory(int catid, int offset, int count) throws DataException
  {
    if (env.getEngine().getParamBoolean(EngineBackend.BP_NOCATEGORIES))
      throw new DataException("The category system has been disabled on this site.");
    return CommunityUserContextImpl.getCommunitiesInCategory(env,catid,offset,count);

  } // end getCommunitiesInCategory

  public List getCommunitiesInCategory(CategoryDescriptor cat, int offset, int count) throws DataException
  {
    if (env.getEngine().getParamBoolean(EngineBackend.BP_NOCATEGORIES))
      throw new DataException("The category system has been disabled on this site.");
    return CommunityUserContextImpl.getCommunitiesInCategory(env,cat.getLinkedCategoryID(),offset,count);

  } // end getCommunitiesInCategory

  public int getNumCommunitiesInCategory(int catid) throws DataException
  {
    if (env.getEngine().getParamBoolean(EngineBackend.BP_NOCATEGORIES))
      throw new DataException("The category system has been disabled on this site.");
    return CommunityUserContextImpl.getNumCommunitiesInCategory(env,catid);

  } // end getNumCommunitiessInCategory

  public int getNumCommunitiesInCategory(CategoryDescriptor cat) throws DataException
  {
    if (env.getEngine().getParamBoolean(EngineBackend.BP_NOCATEGORIES))
      throw new DataException("The category system has been disabled on this site.");
    return CommunityUserContextImpl.getNumCommunitiesInCategory(env,cat.getLinkedCategoryID());

  } // end getNumCommunitiesInCategory

  public List searchForCategories(int mode, String term, int offset, int count) throws DataException
  {
    if (env.getEngine().getParamBoolean(EngineBackend.BP_NOCATEGORIES))
      throw new DataException("The category system has been disabled on this site.");
    return CategoryDescriptorImpl.searchForCategories(env,env.testPermission(EnvUser.PERM_SHOWHIDDENCATS),
						      env.testPermission(EnvUser.PERM_SEARCHHIDDENCATS),mode,
						      term,offset,count);

  } // end searchForCategories

  public int getSearchCategoryCount(int mode, String term) throws DataException
  {
    if (env.getEngine().getParamBoolean(EngineBackend.BP_NOCATEGORIES))
      throw new DataException("The category system has been disabled on this site.");
    return CategoryDescriptorImpl.getSearchCategoryCount(env,env.testPermission(EnvUser.PERM_SHOWHIDDENCATS),
							 env.testPermission(EnvUser.PERM_SEARCHHIDDENCATS),
							 mode,term);

  } // end getSearchCategoryCount

  public CommunityContext createCommunity(String name, String alias, String language, String synopsis,
					  String rules, String joinkey, CommunityHideMode hide_mode)
      throws DataException, AccessError
  {
    env.testPermission(EnvUser.PERM_CREATECOMMUNITY,"You are not authorized to create new communities.");

    // Convert the "hide mode" value into the two hide flags.
    boolean hide_dir = (CommunityHideMode.NONE.equals(hide_mode) ? false : true);
    boolean hide_search = CommunityHideMode.BOTH.equals(hide_mode);

    // Create the new community's database entries and internal data.
    CommunityData new_comm = CommunityCoreData.createCommunity(globalsite,env,name,alias,uid,language,
							       synopsis,rules,joinkey,hide_dir,hide_search);

    // Create the community context we return to the user.
    CommunityContext rc = new CommunityUserContextImpl(env,new_comm);

    // And that's it!  You expected lightning bolts maybe? :-)

    return rc;

  } // end createCommunity

  public boolean canCreateCommunity()
  {
    return env.testPermission(EnvUser.PERM_CREATECOMMUNITY);

  } // end canCreateCommunity

  public List getSideBoxList() throws DataException
  {
    Connection conn = null;
    PreparedStatement stmt = null;
    ArrayList rc = new ArrayList();

    try
    { // retrieve a connection from the data pool
      conn = globalsite.getConnection(null);

      // retrieve the necessary rows from the sideboxes table
      stmt = conn.prepareStatement("SELECT boxid, sequence FROM sideboxes WHERE uid = ? ORDER BY sequence;");
      stmt.setInt(1,uid);
      ResultSet rs = stmt.executeQuery();
      while (rs.next())
      { // create the implementation objects and return them all
	SideBoxDescriptor sbd =
            new SideBoxDescriptorImpl(env,env.getEngine().getMasterSideBoxDescriptor(rs.getInt(1)),
				      rs.getInt(2));
	rc.add(sbd);

      } // end while

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

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

    } // end finally

    return Collections.unmodifiableList(rc);

  } // end getSideBoxList

  public void addSideBox(int id) throws DataException
  {
    if (env.getEngine().getMasterSideBoxDescriptor(id)==null)
      throw new DataException("invalid sidebox ID: " + id);

    Connection conn = null;
    Statement stmt = null;
    PreparedStatement pstmt = null;

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

      stmt.executeUpdate("LOCK TABLES sideboxes WRITE;");
      try
      { // do a quickie query to see if we're already in the sidebox list
	pstmt = conn.prepareStatement("SELECT sequence FROM sideboxes WHERE uid = ? AND boxid = ?;");
	pstmt.setInt(1,uid);
	pstmt.setInt(2,id);
	ResultSet rs = pstmt.executeQuery();
	if (rs.next())
	  return;  // already in sidebox list - this is a no-op

	// find a sequence number for the new entry
	pstmt.close();
	pstmt = conn.prepareStatement("SELECT MAX(sequence) FROM sideboxes WHERE uid = ?;");
	pstmt.setInt(1,uid);
	rs = pstmt.executeQuery();
	if (!(rs.next()))
	  throw new InternalStateError("bogus query result on addSideBox");
	int new_sequence = rs.getInt(1) + 100;

	// add the new record
	pstmt.close();
	pstmt = conn.prepareStatement("INSERT INTO sideboxes (uid, sequence, boxid) VALUES (?, ?, ?);");
	pstmt.setInt(1,uid);
	pstmt.setInt(2,new_sequence);
	pstmt.setInt(3,id);
	pstmt.executeUpdate();

      } // 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 sidebox list: " + e.getMessage(),e);
      throw new DataException("error adding to sidebox list: " + e.getMessage(),e);

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

    } // end finally

  } // end addSideBox

  public List getConferenceHotlist() throws DataException
  {
    return ConferenceUserContextImpl.getUserHotlist(env);

  } // end getConferenceHotlist

  public boolean hasAdminAccess()
  {
    return env.testPermission(EnvUser.PERM_SYSADMINACCESS);

  } // end hasAdminAccess

  public AdminOperations getAdminInterface() throws AccessError
  {
    env.testPermission(EnvUser.PERM_SYSADMINACCESS,"You are not permitted to administer the server.");
    return new AdminOperationsImpl(globalsite,env);

  } // end getAdminInterface

  public Locale getLocale() throws DataException
  {
    if (my_locale==null)
      loadPrefs();

    return my_locale;

  } // end getLocale

  public void setLocale(Locale locale) throws DataException
  {
    Connection conn = null;
    Statement stmt = null;

    try
    { // retrieve a connection from the data pool
      conn = globalsite.getConnection(null);
      stmt = conn.createStatement();

      // create the update statement
      StringBuffer sql = new StringBuffer("UPDATE userprefs SET localeid = '");
      sql.append(SQLUtil.encodeString(locale.toString())).append("' WHERE uid = ").append(uid).append(';');

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

      // replace the locale here
      my_locale = locale;

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

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

    } // end finally

  } // end setLocale

  public TimeZone getTimeZone() throws DataException
  {
    if (my_tz==null)
      loadPrefs();

    return my_tz;

  } // end getTimeZone

  public void setTimeZone(TimeZone timezone) throws DataException
  {
    Connection conn = null;
    Statement stmt = null;

    try
    { // retrieve a connection from the data pool
      conn = globalsite.getConnection(null);
      stmt = conn.createStatement();

      // create the update statement
      StringBuffer sql = new StringBuffer("UPDATE userprefs SET tzid = '");
      sql.append(SQLUtil.encodeString(timezone.getID())).append("' WHERE uid = ").append(uid).append(';');

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

      // replace the locale here
      my_tz = timezone;

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

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

    } // end finally

  } // end setTimeZone

  public String getAuthenticationToken() throws AccessError, DataException
  {
    if (!isLoggedIn())
    { // can't generate an authentication token if we're not authenticated!
      logger.error("UserContext not authenticated, cannot generate auth token");
      throw new AccessError("You cannot generate an authentication token without logging in.");

    } // end if

    // Generate a random authentication string and poke it into the database for this user.
    String tokenauth = Generator.get().generateRandomAuthString();
    Connection conn = null;
    PreparedStatement stmt = null;

    try
    { // retrieve a connection from the data pool
      conn = globalsite.getConnection(null);
      stmt = conn.prepareStatement("UPDATE users SET tokenauth = ? WHERE uid = ?;");
      stmt.setString(1,tokenauth);
      stmt.setInt(2,uid);
      stmt.executeUpdate();

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

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

    } // end finally

    // Build the full authentication token string value.
    int checkvalue = uid ^ tokenauth.hashCode();
    StringBuffer buf = new StringBuffer(AUTH_TOKEN_PREFIX);
    buf.append(uid).append(AUTH_TOKEN_SEP).append(tokenauth).append(AUTH_TOKEN_SEP).append(checkvalue);
    buf.append(AUTH_TOKEN_SEP);
    return buf.toString();

  } // end getAuthenticationToken

  public boolean authenticateWithToken(String token) throws DataException
  {
    if (isLoggedIn())
    { // already authenticated, can't authenticate again
      logger.error("UserContext already authenticated (with uid " + uid + ")");
      throw new InternalStateError("context already authenticated");

    } // end if

    if (logger.isDebugEnabled())
      logger.debug("decoding authtoken: " + token);

    // Pick apart the authentication token value.
    if (!(token.startsWith(AUTH_TOKEN_PREFIX)))
    { // token parse error
      logger.error("Token parse error: prefix not valid");
      return false;

    } // end if

    int xstart = AUTH_TOKEN_PREFIX.length();
    int xend = token.indexOf(AUTH_TOKEN_SEP,xstart);
    if (xend<0)
    { // could not find the UID separator
      logger.error("Token parse error: UID sep not found");
      return false;

    } // end if

    int pending_uid;
    try
    { // get the user ID
      pending_uid = Integer.parseInt(token.substring(xstart,xend));

    } // end try
    catch (NumberFormatException nfe)
    { // we couldn't parse the UID
      logger.error("Token parse error: invalid UID value");
      return false;

    } // end catch

    xstart = xend + 1;
    xend = token.indexOf(AUTH_TOKEN_SEP,xstart);
    if (xend<0)
    { // could not find the auth string separator
      logger.error("Token parse error: auth string sep not found");
      return false;

    } // end if

    String pending_auth = token.substring(xstart,xend);
    if (!(Generator.isValidRandomAuthString(pending_auth)))
    { // the auth string is not valid by the rules under which it was generated
      logger.error("Token parse error: invalid auth string value");
      return false;

    } // end if

    xstart = xend + 1;
    xend = token.indexOf(AUTH_TOKEN_SEP,xstart);
    if (xend<0)
    { // could not find the checkvalue separator
      logger.error("Token parse error: checkvalue sep not found");
      return false;

    } // end if

    int checkvalue;
    try
    { // get the check value
      checkvalue = Integer.parseInt(token.substring(xstart,xend));

    } // end try
    catch (NumberFormatException nfe)
    { // we couldn't parse the checkvalue
      logger.error("Token parse error: invalid checkvalue");
      return false;

    } // end catch

    if (checkvalue!=(pending_uid ^ pending_auth.hashCode()))
    { // the checkvalue does not match what it should - possible corrupted token
      logger.error("Token parse error: checkvalue does not match");
      return false;

    } // end if

    // At this point, we now have a UID and authentication string extracted from the token.
    // Proceed to authenticate.
    if (logger.isDebugEnabled())
      logger.debug("Authenticating user ID#" + pending_uid);

    Connection conn = null;
    PreparedStatement stmt = null;
    AuditRecord ar = null;
      
    try
    { // look for a user record matching this user ID
      conn = globalsite.getConnection(null);
      stmt = conn.prepareStatement("SELECT * FROM users WHERE uid = ?;");
      stmt.setInt(1,pending_uid);
      ResultSet rs = stmt.executeQuery();

      if (!(rs.next()))
      {  // user not found
	logger.error("...user not found");
	ar = new AuditRecord(AuditRecord.LOGIN_FAIL,0,remote_addr,"Bad token UID: " + pending_uid);
	return false;

      } // end if

      if (rs.getBoolean("is_anon"))
      { // can't log in as Anonymous Honyak
	logger.error("...user is the Anonymous Honyak, can't explicitly login");
	ar = new AuditRecord(AuditRecord.LOGIN_FAIL,pending_uid,remote_addr,"Anonymous user");
	return false;

      } // end if

      if (rs.getBoolean("lockout"))
      { // account locked out
	logger.error("...user is locked out by the Admin");
	ar = new AuditRecord(AuditRecord.LOGIN_FAIL,pending_uid,remote_addr,"Account locked out");
	return false;

      } // end if

      // compare the stored token auth value to what we have
      if (!(pending_auth.equals(rs.getString("tokenauth"))))
      { // the auth string is bad - we can't log in
	logger.warn("...invalid authentication string");
	ar = new AuditRecord(AuditRecord.LOGIN_FAIL,pending_uid,remote_addr,"Bad auth-string");
	return false;

      } // end if

      if (logger.isDebugEnabled())
	logger.debug("...authenticated");

      // we're authenticated - load the user data into the context
      loadUserData(rs);

      // update the "last access" time in the database
      stmt = conn.prepareStatement("UPDATE users SET lastaccess = ? WHERE uid = ?;");
      java.util.Date mydate = new java.util.Date();
      SQLUtil.setFullDateTime(stmt,1,mydate);
      stmt.setInt(2,uid);
      stmt.executeUpdate();

      // update the "last access" time in this object
      last_access = mydate;

      // an audit record indicating we logged in OK
      ar = new AuditRecord(AuditRecord.LOGIN_OK,uid,remote_addr);

      if (logger.isDebugEnabled())
	logger.debug("...context loaded, we're ready :-)");

    } // end try
    catch (SQLException e)
    { // database error - this is a DataException
      logger.error("DB error reading user data: " + e.getMessage(),e);
      throw new DataException("unable to access user data: " + e.getMessage(),e);

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

    } // end if

    return true;  // token authentication worked!

  } // end authenticateWithToken

  public Advertisement selectAd()
  {
    // just get a random ad for now
    return AdvertisementImpl.getRandomAd(env);

  } // end selectAd

  public boolean displayPostPictures() throws DataException
  {
    if (flags==null)
      loadPrefs();

    return flags.get(BF_POSTPICTURES);

  } // end if

  public UserProperties getProperties() throws DataException
  {
    if (flags==null)
      loadPrefs();
    return createProperties();

  } // end getProperties

  public void setProperties(UserProperties props) throws DataException
  {
    if (flags==null)
      loadPrefs();
    UserProperties orig = createProperties();
    BitSet delta = storeProperties(props);
    if (delta.length()==0)
      return;

    boolean succeeded = false;
    try
    { // update the properties in the database
      updateProperties(delta);
      succeeded = true;

    } // end try
    finally
    { // make sure the connection is released before we go
      if (!succeeded)
	storeProperties(orig);

    } // end finally

  } // end setProperties

  public boolean canSetUserPhoto() throws DataException
  {
    if (flags==null)
      loadPrefs();
    return !(flags.get(BF_ADM_NOPHOTO));

  } // end canSetUserPhoto

  public List searchPosts(String search_terms, int offset, int count) throws 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");

    ServiceToken conf_token = env.getSCM().getTokenForSymbol(ServiceControl.SVCGRP_COMMUNITY,"Conference");
    Connection conn = null;
    PreparedStatement stmt = null;
    ArrayList rc = new ArrayList();

    try
    { // get a database connection
      conn = globalsite.getConnection(null);
      TopicMessageFoundHelper helper = new TopicMessageFoundHelper(conn);

      // create the SQL statement
      stmt = conn.prepareStatement("SELECT s.sigid, c.confid, t.topicid, t.num, p.postid, p.num, p.creator_uid, "
				   + "p.posted, p.linecount, d.data FROM confs c, sigtoconf s, topics t, posts p, "
				   + "postdata d, users u, sigmember m, sigftrs f LEFT JOIN confmember x "
				   + "ON (c.confid = x.confid AND u.uid = x.uid) WHERE u.uid = ? AND "
				   + "s.sigid = m.sigid AND m.uid = u.uid AND s.sigid = f.sigid AND f.ftr_code = ? "
				   + " AND c.confid = s.confid AND GREATEST(u.base_lvl,m.granted_lvl,s.granted_lvl,"
				   + "IFNULL(x.granted_lvl,0)) >= c.read_lvl AND t.confid = c.confid AND "
				   + "t.topicid = p.topicid AND p.scribble_uid IS NULL AND p.postid = d.postid "
				   + "AND MATCH(d.data) AGAINST (?) LIMIT ?, ?;");
      stmt.setInt(1,uid);
      stmt.setInt(2,conf_token.getIndex());
      stmt.setString(3,search_terms);
      stmt.setInt(4,offset);
      stmt.setInt(5,count + 1);

      // execute the query and load the results into the return arraylist
      ResultSet rs = stmt.executeQuery();
      while (rs.next())
	rc.add(new TopicMessageFoundImpl(helper,rs.getInt(1),rs.getInt(2),rs.getInt(3),rs.getInt(4),
					 rs.getLong(5),rs.getInt(6),rs.getInt(7),
					 SQLUtil.getFullDateTime(rs,8),rs.getInt(9),rs.getString(10)));

    } // 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 DataException
  {
    if (logger.isDebugEnabled())
      logger.debug("getSearchPostCount('" + search_terms + ") entry");
    if (search_terms==null)
      throw new NullPointerException("invalid search terms");

    ServiceToken conf_token = env.getSCM().getTokenForSymbol(ServiceControl.SVCGRP_COMMUNITY,"Conference");
    Connection conn = null;
    PreparedStatement stmt = null;

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

      // create the SQL statement
      stmt = conn.prepareStatement("SELECT COUNT(*) FROM confs c, sigtoconf s, topics t, posts p, postdata d, "
				   + "users u, sigmember m, sigftrs f LEFT JOIN confmember x ON (c.confid = x.confid "
				   + "AND u.uid = x.uid) WHERE u.uid = ? AND s.sigid = m.sigid AND m.uid = u.uid "
				   + "AND s.sigid = f.sigid AND f.ftr_code = ? AND c.confid = s.confid AND "
				   + "GREATEST(u.base_lvl,m.granted_lvl,s.granted_lvl,IFNULL(x.granted_lvl,0)) "
				   + ">= c.read_lvl AND t.confid = c.confid AND t.topicid = p.topicid AND "
				   + "p.scribble_uid IS NULL AND p.postid = d.postid AND MATCH(d.data) AGAINST (?);");
      stmt.setInt(1,uid);
      stmt.setInt(2,conf_token.getIndex());
      stmt.setString(3,search_terms);

      // execute the query and load the results
      ResultSet rs = stmt.executeQuery();
      if (!(rs.next()))
	throw new InternalStateError("query failure in UserContextImpl.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 java.sql.Date getDateOfBirth()
  {
    return m_dob;

  } // end getDateOfBirth

  public void setDateOfBirth(java.sql.Date date) throws DataException
  {
    if (is_anon)
    { // trying to change Anonymous Honyak's (nonexistent) date of birth?
      logger.error("cannot change date of birth of anonymous account");
      throw new DataException("The anonymous account cannot change its date of birth.");

    } // end if

    Connection conn = null;
    PreparedStatement stmt = null;

    try
    { // retrieve a connection from the data pool
      conn = globalsite.getConnection(null);
      stmt = conn.prepareStatement("UPDATE users SET dob = ? WHERE uid = ?;");
      stmt.setDate(1,date);
      stmt.setInt(2,uid);
      stmt.executeUpdate();
      m_dob = date;  // change stored information
      
    } // end try
    catch (SQLException e)
    { // turn SQLException into data exception
      logger.error("DB error changing date of birth: " + e.getMessage(),e);
      throw new DataException("Unable to set user date of birth: " + e.getMessage(),e);

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

    } // end finally

  } // end setDateOfBirth

  public void authenticatePrivileged(String username) throws AccessError, DataException
  {
    if (isLoggedIn())
    { // already authenticated, can't authenticate again
      logger.error("UserContext already authenticated (with uid " + uid + ")");
      throw new InternalStateError("context already authenticated");

    } // end if

    if (logger.isDebugEnabled())
      logger.debug("authenticatePrivileged(): authenticating user \"" + username + "\"...");

    if (!(globalsite.isPrivilegedAddress(remote_addr)))
    { // not privileged - f**k off
      logger.error("source address " + remote_addr + " is not privileged!");
      throw new AccessError("Remote address is not privileged!");

    } // end if

    Connection conn = null;
    PreparedStatement stmt = null;
    AuditRecord ar = null;
      
    try
    { // look for a user name matching this user record
      conn = globalsite.getConnection(null);
      stmt = conn.prepareStatement("SELECT * FROM users WHERE username = ?;");
      stmt.setString(1,username);
      ResultSet rs = stmt.executeQuery();

      if (!(rs.next()))
      {  // user not found
	logger.error("...user not found");
	ar = new AuditRecord(AuditRecord.LOGIN_FAIL,0,remote_addr,"Bad username: " + username);
	throw new AccessError("The user account you have specified does not exist.  Please try again.");

      } // end if

      int the_uid = rs.getInt("uid");

      if (rs.getBoolean("is_anon"))
      { // can't log in as Anonymous Honyak
	logger.error("...user is the Anonymous Honyak, can't explicitly login");
	ar = new AuditRecord(AuditRecord.LOGIN_FAIL,the_uid,remote_addr,"Anonymous user");
	throw new AccessError("This account cannot be explicitly logged into.  Please try again.");

      } // end if

      if (logger.isDebugEnabled())
	logger.debug("...authenticated");

      // we're authenticated - load the user data into the context
      loadUserData(rs);
      rs.close();

      // update the "last access" time in the database
      stmt.close();
      stmt = conn.prepareStatement("UPDATE users SET lastaccess = ? WHERE uid = ?;");
      java.util.Date mydate = new java.util.Date();
      SQLUtil.setFullDateTime(stmt,1,mydate);
      stmt.setInt(2,uid);
      stmt.executeUpdate();

      // update the "last access" time in this object
      last_access = mydate;

      // an audit record indicating we logged in OK
      ar = new AuditRecord(AuditRecord.LOGIN_OK,the_uid,remote_addr,"[privileged]");

      if (logger.isDebugEnabled())
	logger.debug("...context loaded, we're ready :-)");

    } // end try
    catch (SQLException e)
    { // database error - this is a DataException
      logger.error("DB error reading user data: " + e.getMessage(),e);
      throw new DataException("unable to access user data: " + e.getMessage(),e);

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

    } // end if

  } // end authenticatePrivileged

  /*--------------------------------------------------------------------------------
   * Implementations from interface ServiceProvider
   *--------------------------------------------------------------------------------
   */

  public Object queryService(Class klass)
  {
    if (klass==UserContext.class)
      return ((UserContext)this);
    if (klass==PropertyProvider.class)
      return ((PropertyProvider)this);
    throw new NoSuchServiceException("UserContext",klass);

  } // end queryService

  public Object queryService(Class klass, String serviceid)
  {
    return this.queryService(klass);

  } // end queryService

  /*--------------------------------------------------------------------------------
   * Implementations from interface PropertyProvider
   *--------------------------------------------------------------------------------
   */

  public String propGet(String identifier)
  {
    try
    { // these properties have to be retrieved specially
      if (identifier.equals("fullname"))
      { // get the full name of the user
	if (full_name==null)
	  getContactInfo();
	return full_name;

      } // end if
      else if (identifier.equals("email"))
      { // get the E-mail address
	if (my_email==null)
	  getContactInfo();
	return my_email;

      } // end else if
      else if (identifier.equals("default.pseud"))
      { // get the default pseud
	if (my_pseud==null)
	  getContactInfo();
	return my_pseud;

      } // end else if

    } // end try
    catch (DataException de)
    { // if we hit a DataException, return an erroneous value
      logger.error("DataException in propGet()",de);
      return "<<error>>";

    } // end catch

    if (identifier.equals("id"))
      return String.valueOf(uid);
    else if (identifier.equals("name"))
      return username;
    else if (identifier.equals("ip.address"))
      return remote_addr;
    else if (identifier.equals("base.level"))
      return String.valueOf(level);
    else if (identifier.equals("is.anonymous"))
      return String.valueOf(is_anon);

    throw new PropertyNotFoundException("UserContext",identifier);

  } // end propGet

  public int propGetInt(String identifier)
  {
    if (identifier.equals("id"))
      return uid;
    else if (identifier.equals("base.level"))
      return level;
    throw new PropertyNotFoundException("UserContext",identifier);

  } // end propGetInt

  public boolean propGetBoolean(String identifier)
  {
    if (identifier.equals("is.anonymous"))
      return is_anon;
    throw new PropertyNotFoundException("UserContext",identifier);

  } // end propGetBoolean

  /*--------------------------------------------------------------------------------
   * Operations private to implementation package
   *--------------------------------------------------------------------------------
   */

  void loadAnonymous(String remote_addr) throws DataException
  {
    Connection conn = null;
    Statement stmt = null;
    if (logger.isDebugEnabled())
      logger.debug("loadAnonymous() on UserContext: addr " + remote_addr);

    try
    { // retrieve a connection from the data pool
      conn = globalsite.getConnection(null);
      stmt = conn.createStatement();
      ResultSet rs = stmt.executeQuery("SELECT * FROM users WHERE is_anon = 1;");
      if (!(rs.next()))
      { // could not find the anonymous user - this is an internal error
	logger.error("WTF? anonymous user not found?!?");
	throw new DataException("INTERNAL ERROR - cannot find anonymous user");

      } // end if

      loadUserData(rs);  // load the data for Anonymous Honyak

      this.remote_addr = remote_addr;

    } // end try
    catch (SQLException e)
    { // database error - this is a DataException
      logger.error("error loading anonymous user data: " + e.getMessage(),e);
      throw new DataException("unable to access user data: " + e.getMessage(),e);

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

    } // end finally

  } // end loadAnonymous

  void loadNewUser(String remote_addr, int uid, int level, String username, int confirm_num,
		   java.util.Date created, java.util.Date last_access, java.sql.Date dob)
  {
    if (logger.isDebugEnabled())
      logger.debug("loadNewUser() on UserContext: addr " + remote_addr + ", uid " + uid + ", level "
		   + level + ", username \"" + username + "\"");

    this.remote_addr = remote_addr;
    this.uid = uid;
    this.contactid = -1;
    this.level = level;
    this.username = username;
    this.confirm_num = confirm_num;
    this.created = created;
    this.last_access = last_access;
    m_dob = dob;

  } // end loadNewUser

  void autoJoinCommunities() throws DataException
  {
    Connection conn = null;

    try
    { // get a database connection and call the internal function
      conn = globalsite.getConnection(null);
      autoJoinCommunities(conn);

    } // end try
    catch (SQLException e)
    { // database error - this is a DataException
      logger.error("error autojoining communities: " + e.getMessage(),e);
      throw new DataException("unable to autojoin communities: " + e.getMessage(),e);

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

    } // end finally

  } // end autoJoinCommunities
  
  /*--------------------------------------------------------------------------------
   * External static operations
   *--------------------------------------------------------------------------------
   */

  static final ReturnNewUser createAccount(EnvEngine env, String remote_addr, String username, String password,
					   boolean prehashed, String reminder, boolean verify_email, boolean lockout,
					   int confirm_num, Role base_role, String description, java.sql.Date dob)
      throws AccessError, DataException
  {
    int new_uid;                // new user ID - return from this function
    java.util.Date created;     // date created - return from this function
    Connection conn = null;
    Statement stmt = null;
    PreparedStatement pstmt = null;
    AuditRecord ar = null;

    try
    { // first, lock a bunch of tables for the purpose of this operation
      conn = env.getConnection();
      stmt = conn.createStatement();
      stmt.executeUpdate("LOCK TABLES users WRITE, userprefs WRITE, propuser WRITE, sigmember WRITE, "
			 + "sideboxes WRITE, confhotlist WRITE;");

      try
      { // make sure the user account doesn't already exist
	pstmt = conn.prepareStatement("SELECT uid FROM users WHERE username = ?;");
	pstmt.setString(1,username);
	ResultSet rs = pstmt.executeQuery();
	if (rs.next())
	{ // the user account already exists
	  logger.warn("username \"" + username + "\" already exists");
	  throw new AccessError("That user name already exists.  Please try again.");

	} // end if

	rs.close();

	// Insert a new record for this user 
	String hash_value = (prehashed ? password : Generator.hashPassword(password));
	pstmt.close();
	pstmt = conn.prepareStatement("INSERT INTO users (username, passhash, verify_email, lockout, email_confnum, "
				      + "base_lvl, created, lastaccess, passreminder, description, dob) VALUES "
				      + "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);");
	pstmt.setString(1,username);
	pstmt.setString(2,hash_value);
	pstmt.setInt(3,verify_email ? 1 : 0);
	pstmt.setInt(4,lockout ? 1 : 0);
	pstmt.setInt(5,confirm_num);
	pstmt.setInt(6,base_role.getLevel());
	created = new java.util.Date();
	SQLUtil.setFullDateTime(pstmt,7,created);
	SQLUtil.setFullDateTime(pstmt,8,created);
	pstmt.setString(9,reminder);
	pstmt.setString(10,description);
	pstmt.setDate(11,dob);
	pstmt.executeUpdate();

	// what is the new user ID?
	rs = stmt.executeQuery("SELECT LAST_INSERT_ID();");
	if (!(rs.next()))
	{ // the readback failed...
	  logger.error("readback of new user's UID failed");
	  throw new DataException("unable to read back new user ID");

	} // end if

	new_uid = rs.getInt(1);
	if (logger.isDebugEnabled())
	  logger.debug("...created user \"" + username + "\" with UID " + new_uid);
	rs.close();

	// add a UserPrefs record for this user, too
	pstmt.close();
	pstmt = conn.prepareStatement("INSERT INTO userprefs (uid) VALUES (?);");
	pstmt.setInt(1,new_uid);
	pstmt.executeUpdate();

	if (logger.isDebugEnabled())
	  logger.debug("...created userprefs");

	// add a properties configuration for this user
	rs = stmt.executeQuery("SELECT propuser.ndx, propuser.data FROM propuser, users WHERE "
                               + "propuser.uid = users.uid AND users.is_anon = 1;");
	pstmt.close();
	pstmt = conn.prepareStatement("INSERT INTO propuser (uid, ndx, data) VALUES (?, ?, ?);");
	while (rs.next())
	{ // set up to insert into the propuser table
	  pstmt.setInt(1,new_uid);
	  pstmt.setInt(2,rs.getInt(1));
	  pstmt.setString(3,rs.getString(2));
	  pstmt.executeUpdate();

	} // end while

	if (logger.isDebugEnabled())
	  logger.debug("...created user properties");

	// get the sidebox configuration for this user
	rs = stmt.executeQuery("SELECT sideboxes.boxid, sideboxes.sequence FROM sideboxes, "
			       + "users WHERE sideboxes.uid = users.uid AND users.is_anon = 1;");
	pstmt.close();
	pstmt = conn.prepareStatement("INSERT INTO sideboxes (uid, boxid, sequence) VALUES (?, ?, ?);");
	while (rs.next())
	{ // set up to insert into the sideboxes table
	  pstmt.setInt(1,new_uid);
	  pstmt.setInt(2,rs.getInt(1));
	  pstmt.setInt(3,rs.getInt(2));
	  pstmt.executeUpdate();

	} // end while

	if (logger.isDebugEnabled())
	  logger.debug("...loaded default sidebox config");

	// get the hotlist configuration for this user
	rs = stmt.executeQuery("SELECT confhotlist.sequence, confhotlist.sigid, confhotlist.confid FROM "
		             + "confhotlist, users WHERE confhotlist.uid = users.uid AND users.is_anon = 1;");
	pstmt.close();
	pstmt = conn.prepareStatement("INSERT INTO confhotlist (uid, sequence, sigid, confid) VALUES (?, ?, ?, ?);");
	while (rs.next())
	{ // set up to insert into the confhotlist table
	  pstmt.setInt(1,new_uid);
	  pstmt.setInt(2,rs.getInt(1));
	  pstmt.setInt(3,rs.getInt(2));
	  pstmt.setInt(4,rs.getInt(3));
	  pstmt.executeUpdate();

	} // end while

	if (logger.isDebugEnabled())
	  logger.debug("...loaded default hotlist config");

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

      } // end finally

      // the operation was a success - give back an audit record
      ar = new AuditRecord(AuditRecord.ACCOUNT_CREATE,new_uid,remote_addr);

    } // end try
    catch (SQLException e)
    { // database error - this is a DataException
      logger.error("DB error creating user: " + e.getMessage(),e);
      throw new DataException("unable to create user record: " + e.getMessage(),e);

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

    } // end finally

    return new ReturnNewUser(new_uid,created);
    
  } // end createAccount

  /*--------------------------------------------------------------------------------
   * Static initializer
   *--------------------------------------------------------------------------------
   */

  static
  { // initialize the "all properties" bit set
    BitSet tmp = new BitSet();
    tmp.set(PROP_FLAGS);
    ALL_PROPS = tmp;

  } // end static initializer

} // end class UserContextImpl
