/*
 * 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.awt.Dimension;
import java.sql.*;
import java.util.*;
import org.apache.log4j.*;
import org.w3c.dom.*;
import com.silverwrist.util.*;
import com.silverwrist.util.cache.*;
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.*;
import com.silverwrist.venice.svc.*;
import com.silverwrist.venice.util.MailSend;
import com.silverwrist.venice.util.ServiceProvider;
import com.silverwrist.venice.util.XMLLoader;

public class VeniceEngineImpl implements VeniceEngine, ServiceProvider, EngineBackend
{
  /*--------------------------------------------------------------------------------
   * Internal class storing side box information.
   *--------------------------------------------------------------------------------
   */

  class MasterSideBox implements SideBoxDescriptor
  {
    private int id;
    private String title;
    private String anon_title;
    private String classname;

    MasterSideBox(Element section) throws ConfigException
    {
      XMLLoader loader = XMLLoader.get();
      id = loader.configGetAttributeInt(section,"id");

      // get the title and classname
      DOMElementHelper section_h = new DOMElementHelper(section);
      title = loader.configGetSubElementText(section_h,"title");
      classname = loader.configGetSubElementText(section_h,"factory-class");

      try
      { // try to resolve the classname here so we can be sure it's cool
	Class.forName(classname);

      } // end try
      catch (ClassNotFoundException e)
      { // the class can't be resolved - we are h0sed
	logger.fatal("<sidebox/> config <factory-class/> not valid!");
	throw new ConfigException("<factory-class/> in <sidebox/> is not a valid class!",section);

      } // end catch

      // get the anonymous title and default it if it's not found
      anon_title = section_h.getSubElementText("anon-title");
      if (StringUtil.isStringEmpty(anon_title))
	anon_title = title;  // just set as the same in the event of difficulty

    } // end constructor

    public int getID()
    {
      return id;

    } // end getID

    public int getSideBoxID()
    {
      return id;

    } // end getSideBoxID

    public int getSequence()
    {
      return -1;

    } // end getSequence

    public String getTitle(boolean anonymous)
    {
      return (anonymous ? anon_title : title);

    } // end getTitle

    public String getFactoryClassName()
    {
      return classname;

    } // end getClassname

  } // end class MasterSideBox

  /*--------------------------------------------------------------------------------
   * Internal class for creating new CommunityCoreData objects.
   *--------------------------------------------------------------------------------
   */

  protected class CommunityCoreDataCreator implements ObjectFactory
  {
    protected CommunityCoreDataCreator()
    { // do nothing
    } // end constructor

    public Object newObject(Object key) throws ObjectFactoryException
    {
      Integer xcid = (Integer)key;
      try
      { // create the desired object
	return new CommunityCoreData(globalsite,env,xcid.intValue());

      } // end try
      catch (DataException e)
      { // rethrow as a "wrapped" exception
	throw new ObjectFactoryException(e);

      } // end catch

    } // end build

  } // end class CommunityCoreDataCreator

  /*--------------------------------------------------------------------------------
   * Internal class for creating new ConferenceCoreData objects.
   *--------------------------------------------------------------------------------
   */

  protected class ConferenceCoreDataCreator implements ObjectFactory
  {
    protected ConferenceCoreDataCreator()
    { // do nothing
    } // end constructor

    public Object newObject(Object key) throws ObjectFactoryException
    {
      Integer xconf = (Integer)key;
      try
      { // create the desired object
	return new ConferenceCoreData(globalsite,env,xconf.intValue());

      } // end try
      catch (DataException e)
      { // rethrow as a "wrapped" exception
	throw new ObjectFactoryException(e);

      } // end catch

    } // end build

  } // end class ConferenceCoreDataCreator

  /*--------------------------------------------------------------------------------
   * Internal class for password change requests
   *--------------------------------------------------------------------------------
   */

  class PasswordChangeRequest
  {
    private static final long EXPIRE_TIME = 3600000L;  // one hour

    private int uid;                    // UID of the password change request
    private String username;            // username of the password change request
    private int authentication;         // authentication value
    private String email;               // email address
    private long timestamp;             // current timestamp

    PasswordChangeRequest(int uid, String username, String email)
    {
      this.uid = uid;
      this.username = username;
      this.email = email;
      this.authentication = Generator.get().getAuthorizationNumber();
      this.timestamp = System.currentTimeMillis();

    } // end constructor

    final int getUID()
    {
      return uid;

    } // end getUID

    final String getUserName()
    {
      return username;

    } // end getUserName

    final int getAuthentication()
    {
      return authentication;

    } // end getAuthentication

    final String getEmail()
    {
      return email;

    } // end getEmail

    final boolean isExpired()
    {
      long diff = System.currentTimeMillis() - timestamp;
      return (diff>EXPIRE_TIME);

    } // end isExpired
    
  } // end class PasswordChangeRequest

  /*--------------------------------------------------------------------------------
   * Static data values
   *--------------------------------------------------------------------------------
   */

  // Property indices
  private static final int PROP_FLAGS = 0;       // flags

  private static final int _PPROP_IP_BASE = 256; // base for "pseudo property" indexes

  private static final BitSet IP_PROPS;          // BitSet representing the "integer parameter" properties
  private static final BitSet ALL_PROPS;         // BitSet representing all properties

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

  private static final Dimension DEFAULT_DIM_USERPHOTO = new Dimension(100,100);
  private static final Dimension DEFAULT_DIM_COMMUNITYLOGO = new Dimension(110,65);

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

  private GlobalSiteImpl globalsite = null;           // my global site
  private EnvEngine env = null;                       // my environment
  private ObjectCache comm_objcache = new ObjectCache(new CommunityCoreDataCreator());
  private ObjectCache conf_objcache = new ObjectCache(new ConferenceCoreDataCreator());
  private int[] gp_ints;                              // global integer parameters
  private MasterSideBox[] sideboxes;                  // master sidebox table
  private Hashtable sidebox_ids = new Hashtable();    // maps sidebox IDs to MasterSideBox objects
  private LinkedList cache_fp_posts = new LinkedList();  // all posts that have been published to front page
  private boolean cache_fp_posts_busy = false;        // busy flag for above vector
  private HashSet no_compress_types = new HashSet();  // the file types that can't be compressed
  private HashMap password_changes = new HashMap();   // current password change requests
  private OptionSet global_flags = new OptionSet();   // global option flags
  private ServiceControlManager scmgr;                // service control manager

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

  public VeniceEngineImpl()
  { // do nothing
  } // end constructor

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

  private final void checkInitialized()
  {
    if (globalsite==null)
      throw new InternalStateError("Venice engine not initialized!");

  } // end checkInitialized

  private final void loadDefaults(Statement stmt) throws SQLException, DataException
  {
    final String query1 = "SELECT ndx, data FROM propglobal;";
    ResultSet rs = stmt.executeQuery(query1);
    while (rs.next())
    { // load the property values based on indexes
      switch (rs.getInt(1))
      {
	case PROP_FLAGS:
	  global_flags.assign(rs.getString(2));
	  break;

	default:
	  break;  // don't understand property index - ignored

      } // end switch

    } // end while

    SQLUtil.shutdown(rs);

    final String query2 =
        "SELECT posts_per_page, old_posts_at_top, max_search_page, max_sig_mbr_page, max_conf_mbr_page, "
      + "fp_posts, num_audit_page, sig_create_lvl FROM globals;";
    rs = stmt.executeQuery(query2);
    if (!(rs.next()))
      throw new DataException("Globals table does not appear to be loaded!");

    // stash the globals from the database
    gp_ints[IP_POSTSPERPAGE] = rs.getInt(1);
    gp_ints[IP_POSTSATTOP] = rs.getInt(2);
    gp_ints[IP_MAXSEARCHRETURN] = rs.getInt(3);
    gp_ints[IP_MAXCOMMUNITYMEMBERDISPLAY] = rs.getInt(4);
    gp_ints[IP_MAXCONFMEMBERDISPLAY] = rs.getInt(5);
    gp_ints[IP_NUMFRONTPAGEPOSTS] = rs.getInt(6);
    gp_ints[IP_NUMAUDITRECSPERPAGE] = rs.getInt(7);
    gp_ints[IP_CREATECOMMUNITYLVL] = rs.getInt(8);
    
    SQLUtil.shutdown(rs);

  } // end loadDefaults

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

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

      if (delta.get(PROP_FLAGS))
      { // update the global flags
	sql.append("UPDATE propglobal SET data = '").append(global_flags.asString()).append("' WHERE ndx = ");
	sql.append(PROP_FLAGS).append(";");
	stmt.executeUpdate(sql.toString());
	sql.setLength(0);

      } // end if

      BitSet tmp = (BitSet)(delta.clone());
      tmp.and(IP_PROPS);
      if (tmp.length()>0)
      { // need to update the globals table
	sql.append("UPDATE globals SET");
	if (delta.get(_PPROP_IP_BASE + IP_POSTSPERPAGE))
	  sql.append(" posts_per_page = ").append(gp_ints[IP_POSTSPERPAGE]).append(',');
	if (delta.get(_PPROP_IP_BASE + IP_POSTSATTOP))
	  sql.append(" old_posts_at_top = ").append(gp_ints[IP_POSTSATTOP]).append(',');
	if (delta.get(_PPROP_IP_BASE + IP_MAXSEARCHRETURN))
	  sql.append(" max_search_page = ").append(gp_ints[IP_MAXSEARCHRETURN]).append(',');
	if (delta.get(_PPROP_IP_BASE + IP_MAXCOMMUNITYMEMBERDISPLAY))
	  sql.append(" max_sig_mbr_page = ").append(gp_ints[IP_MAXCOMMUNITYMEMBERDISPLAY]).append(',');
	if (delta.get(_PPROP_IP_BASE + IP_MAXCONFMEMBERDISPLAY))
	  sql.append(" max_conf_mbr_page = ").append(gp_ints[IP_MAXCONFMEMBERDISPLAY]).append(',');
	if (delta.get(_PPROP_IP_BASE + IP_NUMFRONTPAGEPOSTS))
	  sql.append(" fp_posts = ").append(gp_ints[IP_NUMFRONTPAGEPOSTS]).append(',');
	if (delta.get(_PPROP_IP_BASE + IP_NUMAUDITRECSPERPAGE))
	  sql.append(" num_audit_page = ").append(gp_ints[IP_NUMAUDITRECSPERPAGE]).append(',');
	if (delta.get(_PPROP_IP_BASE + IP_CREATECOMMUNITYLVL))
	  sql.append(" sig_create_lvl = ").append(gp_ints[IP_CREATECOMMUNITYLVL]).append(',');
	sql.setLength(sql.length()-1);  // trim off the last comma
	sql.append(';');
	stmt.executeUpdate(sql.toString());
	sql.setLength(0);

      } // end if

    } // end try
    catch (SQLException e)
    { // convert exceptions to DataException
      logger.fatal("Database error storing defaults: " + e.getMessage(),e);
      throw new DataException("error saving new defaults: " + e.getMessage(),e);

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

    } // end finally

  } // end updateDefaults

  private final synchronized void updateDefaults() throws DataException
  {
    updateDefaults(ALL_PROPS);

  } // end updateDefaults

  private final GlobalProperties createProperties()
  {
    GlobalProperties rc = new GlobalProperties();
    rc.setPostsPerPage(gp_ints[IP_POSTSPERPAGE]);
    rc.setOldPostsAtTop(gp_ints[IP_POSTSATTOP]);
    rc.setSearchItemsPerPage(gp_ints[IP_MAXSEARCHRETURN]);
    rc.setCommunityMembersPerPage(gp_ints[IP_MAXCOMMUNITYMEMBERDISPLAY]);
    rc.setConferenceMembersPerPage(gp_ints[IP_MAXCONFMEMBERDISPLAY]);
    rc.setPostsOnFrontPage(gp_ints[IP_NUMFRONTPAGEPOSTS]);
    rc.setAuditRecordsPerPage(gp_ints[IP_NUMAUDITRECSPERPAGE]);
    rc.setCommunityCreateLevel(gp_ints[IP_CREATECOMMUNITYLVL]);
    rc.setDisplayPostPictures(global_flags.get(BP_POSTPICTURES));
    rc.setDisableCategories(global_flags.get(BP_NOCATEGORIES));
    return rc;

  } // end createProperties

  private final BitSet storeProperties(GlobalProperties props)
  {
    BitSet rc = new BitSet();
    if (global_flags.assign(BP_POSTPICTURES,props.getDisplayPostPictures()))
      rc.set(PROP_FLAGS);
    if (global_flags.assign(BP_NOCATEGORIES,props.getDisableCategories()))
      rc.set(PROP_FLAGS);
    if (gp_ints[IP_POSTSPERPAGE]!=props.getPostsPerPage())
    { // record variable change
      gp_ints[IP_POSTSPERPAGE] = props.getPostsPerPage();
      rc.set(_PPROP_IP_BASE + IP_POSTSPERPAGE);

    } // end if

    if (gp_ints[IP_POSTSATTOP]!=props.getOldPostsAtTop())
    { // record variable change
      gp_ints[IP_POSTSATTOP] = props.getOldPostsAtTop();
      rc.set(_PPROP_IP_BASE + IP_POSTSATTOP);

    } // end if

    if (gp_ints[IP_MAXSEARCHRETURN]!=props.getSearchItemsPerPage())
    { // record variable change
      gp_ints[IP_MAXSEARCHRETURN] = props.getSearchItemsPerPage();
      rc.set(_PPROP_IP_BASE + IP_MAXSEARCHRETURN);

    } // end if

    if (gp_ints[IP_MAXCOMMUNITYMEMBERDISPLAY]!=props.getCommunityMembersPerPage())
    { // record variable change
      gp_ints[IP_MAXCOMMUNITYMEMBERDISPLAY] = props.getCommunityMembersPerPage();
      rc.set(_PPROP_IP_BASE + IP_MAXCOMMUNITYMEMBERDISPLAY);

    } // end if

    if (gp_ints[IP_MAXCONFMEMBERDISPLAY]!=props.getConferenceMembersPerPage())
    { // record variable change
      gp_ints[IP_MAXCONFMEMBERDISPLAY] = props.getConferenceMembersPerPage();
      rc.set(_PPROP_IP_BASE + IP_MAXCONFMEMBERDISPLAY);

    } // end if

    if (gp_ints[IP_NUMFRONTPAGEPOSTS]!=props.getPostsOnFrontPage())
    { // record variable change
      gp_ints[IP_NUMFRONTPAGEPOSTS] = props.getPostsOnFrontPage();
      rc.set(_PPROP_IP_BASE + IP_NUMFRONTPAGEPOSTS);

    } // end if

    if (gp_ints[IP_NUMAUDITRECSPERPAGE]!=props.getAuditRecordsPerPage())
    { // record variable change
      gp_ints[IP_NUMAUDITRECSPERPAGE] = props.getAuditRecordsPerPage();
      rc.set(_PPROP_IP_BASE + IP_NUMAUDITRECSPERPAGE);

    } // end if

    if (gp_ints[IP_CREATECOMMUNITYLVL]!=props.getCommunityCreateLevel())
    { // record variable change
      gp_ints[IP_CREATECOMMUNITYLVL] = props.getCommunityCreateLevel();
      rc.set(_PPROP_IP_BASE + IP_CREATECOMMUNITYLVL);

    } // end if

    return rc;

  } // end storeProperties

  /*--------------------------------------------------------------------------------
   * Implementations from interface VeniceEngine
   *--------------------------------------------------------------------------------
   */

  /**
   * Loads the Venice engine configuration from the named XML file and creates appropriate subobjects,
   * such as the data pool.
   *
   * @param config The XML document containing the configuration information for the engine.
   * @param app_root The root directory of the Web application, which ends with a "/".
   * @exception com.silverwrist.venice.core.ConfigException An error occurred during engine configuration;
   *                                                        the engine could not be initialized.
   * @exception com.silverwrist.venice.core.DataException The Venice engine data could not be loaded.
   */
  public void initialize(Document config, String app_root) throws ConfigException, DataException
  {
    int i;  // loop counter

    if (globalsite!=null)
    { // already configured!
      logger.error("Venice engine already initialized");
      throw new ConfigException("Venice engine already initialized");

    } // end if

    try
    { // first, verify that this is a valid configuration
      XMLLoader loader = XMLLoader.get();
      Element root = loader.configGetRootElement(config,"venice-config");
      DOMElementHelper root_h = new DOMElementHelper(root);

      // Get the <engine/> section.
      Element sect = loader.configGetSubSection(root_h,"engine");
      DOMElementHelper sect_h = new DOMElementHelper(sect);

      // Get the name of the sidebox config file.
      String sidebox_config = loader.configGetSubElementText(sect_h,"sidebox-config");
      if (!(sidebox_config.startsWith("/")))
	sidebox_config = app_root + sidebox_config;

      // Get the name of the services config file.
      String services_config = loader.configGetSubElementText(sect_h,"services-config");
      if (!(services_config.startsWith("/")))
	services_config = app_root + services_config;

      // Create the global site.
      globalsite = new GlobalSiteImpl(this,root,app_root);

      // Get the <upload/> section.
      sect = loader.configGetSubSection(root_h,"upload");
      sect_h = new DOMElementHelper(sect);

      // Look for a "no-compress" blacklist.
      Element subsect = sect_h.getSubElement("no-compress");
      NodeList nl;
      if (subsect!=null)
      { // Initialize the no-compress list.
	nl = subsect.getChildNodes();
	for (i=0; i<nl.getLength(); i++)
	{ // retrieve all entries from the list
	  Node n = nl.item(i);
	  if (n.getNodeType()==Node.ELEMENT_NODE)
	  { // look at the element name now
	    loader.configVerifyNodeName(n,"type",subsect);
	    DOMElementHelper h = new DOMElementHelper((Element)n);
	    no_compress_types.add(h.getElementText().trim().toLowerCase());

	  } // end if
	  // else skip this node

	} // end for

      } // end if
      // else no list, just proceed

      // Now done with the master config...
      // Load the sidebox configuration file.
      Document subdoc = loader.loadConfigDocument(sidebox_config);
      root = loader.configGetRootElement(subdoc,"sidebox-config");

      // Load the individual sideboxes.
      ArrayList sidebox_tmp = new ArrayList();
      nl = root.getChildNodes();
      for (i=0; i<nl.getLength(); i++)
      { // extract each sidebox section from the list and build a new master sidebox object
	Node n = nl.item(i);
	if ((n.getNodeType()==Node.ELEMENT_NODE) && (n.getNodeName().equals("sidebox")))
	  sidebox_tmp.add(new MasterSideBox((Element)n));

      } // end for

      // store the real master sidebox table as an array
      sideboxes = (MasterSideBox[])(sidebox_tmp.toArray(new MasterSideBox[0]));
      if (logger.isDebugEnabled())
	logger.debug(sideboxes.length + " sidebox definitions loaded from XML");

      // Now done with the sidebox config...
      // Load the services config file.
      subdoc = loader.loadConfigDocument(services_config);
      scmgr = new ServiceControlManager(globalsite,subdoc);

    } // end try
    catch (ConfigException ce)
    { // before we leave on a ConfigException, nuke the important data
      if (globalsite!=null)
	globalsite.shutdown();
      throw ce;

    } // end catch

    // Initialize the environment.
    env = new EnvEngine(globalsite,this,scmgr);

    for (i=0; i<sideboxes.length; i++) // insert sideboxes into hashtable
      sidebox_ids.put(new Integer(sideboxes[i].getID()),sideboxes[i]);

    // Allocate the global parameter arrays.
    gp_ints = new int[IPC_NUM_PARAMS];

    // initialize anything that requires us to pull from the database
    Connection conn = null;
    Statement stmt = null;
    try
    { // get a connection from the data pool
      conn = globalsite.getConnection(null);
      stmt = conn.createStatement();

      // load the global defaults
      loadDefaults(stmt);

    } // end try
    catch (SQLException e)
    { // convert exceptions to DataException
      logger.fatal("Database error loading engine data: " + e.getMessage(),e);
      throw new DataException("error initializing Venice engine: " + e.getMessage(),e);

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

    } // end finally

    if (logger.isDebugEnabled())
      logger.debug("initialize() complete :-)");

  } // end initialize

  public void shutdown() throws ConfigException
  {
    if (globalsite==null)
    { // already configured!
      logger.error("Venice engine not initialized");
      throw new ConfigException("Venice engine not initialized");

    } // end if

    scmgr.shutdown();
    scmgr = null;
    env = null;
    globalsite.shutdown();
    globalsite = null;

  } // end shutdown

  public UserContext createUserContext(String remote_addr) throws DataException
  {
    checkInitialized();
    UserContextImpl uci = new UserContextImpl(globalsite,env);
    uci.loadAnonymous(remote_addr);
    if (logger.isDebugEnabled())
      logger.debug("createUserContext(): context loaded :-)");
    return (UserContext)uci;

  } // end createUserContext

  public String getEmailAddressForUser(String username) throws DataException, AccessError
  {
    checkInitialized();
    Connection conn = null;
    Statement stmt = null;
      
    try
    { // look for a user name matching this user record
      conn = globalsite.getConnection(null);
      stmt = conn.createStatement();
      ResultSet rs = stmt.executeQuery("SELECT c.email FROM users u, contacts c WHERE u.contactid = "
				       + "c.contactid AND u.username = '" + SQLUtil.encodeString(username)
				       + "';");
      if (!(rs.next()))
      { // could not find email address
	logger.warn("getEmailAddressForUser(\"" + username + "\") => not found!");
	throw new AccessError("Unable to find email address for user \"" + username +
			      "\".  Please contact the system administrator for assistance.");

      } // end if

      String rc = rs.getString(1);
      if (logger.isDebugEnabled())
	logger.debug("getEmailAddressForUser(\"" + username + "\") => \"" + rc + "\"");
      return rc;

    } // end try
    catch (SQLException e)
    { // database error - this is a DataException
      logger.error("DB error getting email address for user: " + 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 getEmailAddressForUser

  public void sendPasswordReminder(String username) throws DataException, AccessError, EmailException
  {
    checkInitialized();
    Connection conn = null;
    Statement stmt = null;
      
    try
    { // look for a user name matching this user record
      conn = globalsite.getConnection(null);
      stmt = conn.createStatement();
      StringBuffer sql = new StringBuffer("SELECT c.email, u.uid, u.passreminder FROM users u, contacts c "
					  + "WHERE u.contactid = c.contactid AND u.username = '");
      sql.append(SQLUtil.encodeString(username)).append("';");
      ResultSet rs = stmt.executeQuery(sql.toString());
      if (!(rs.next()))
      { // the email address could not be found
	logger.error("can't find email to send password reminder to \"" + username + "\"");
	throw new AccessError("Unable to find email address for user \"" + username +
			      "\".  Please contact the system administrator for assistance.");

      } // end if

      String email_addr = rs.getString(1);
      if (logger.isDebugEnabled())
	logger.debug("sendPasswordReminder(\"" + username + "\") going out to \"" + email_addr + "\"");

      // Enqueue a password change request.
      PasswordChangeRequest pcr = new PasswordChangeRequest(rs.getInt(2),username,email_addr);
      password_changes.put(new Integer(pcr.getUID()),pcr);

      // Create the message to be sent.
      String message = globalsite.getStockMessage("reminder");
      if (message==null)
      { // what? the message does not exist?
	logger.error("\"reminder\" message does not exist in config file!");
	throw new DataException("INTERNAL: reminder message not specified");

      } // end if

      // Replace the message variables.
      HashMap vars = new HashMap(4);
      vars.put("username",username);
      vars.put("reminder",rs.getString(3));
      vars.put("change.uid",String.valueOf(pcr.getUID()));
      vars.put("change.auth",String.valueOf(pcr.getAuthentication()));
      message = StringUtil.replaceAllVariables(message,vars);

      // Find the message subject.
      String subject = globalsite.getStockMessage("reminder-subject");
      if (subject==null)
	subject = "Venice Password Reminder Message";

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

    } // end try
    catch (SQLException e)
    { // database error - this is a DataException
      logger.error("DB error getting email address/reminder for user: " + 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 sendPasswordReminder

  public void completePasswordChange(int uid, int authentication)
      throws DataException, AccessError, EmailException
  {
    checkInitialized();
    Integer key = new Integer(uid);
    PasswordChangeRequest pcr;
    synchronized (this)
    { // make sure we can't pull the same request out twice!
      pcr = (PasswordChangeRequest)(password_changes.get(key));
      if (pcr==null)
	throw new AccessError("Password change request not found.");
      password_changes.remove(key);  // can't use the same request twice!

    } // end synchronized block

    if (authentication!=pcr.getAuthentication())
      throw new AccessError("Invalid password change request.");
    if (pcr.isExpired())
      throw new AccessError("Password change request has expired.");

    if (logger.isDebugEnabled())
      logger.debug("completePasswordChange for user \"" + pcr.getUserName() + "\"");

    // Request is valid; synthesize a new password
    String new_password = Generator.get().generateRandomPassword();
    String hash_value = Generator.hashPassword(new_password);
    Connection conn = null;
    Statement stmt = null;
    try
    { // perform the database update
      conn = globalsite.getConnection(null);
      stmt = conn.createStatement();
      StringBuffer sql = new StringBuffer("UPDATE users SET passhash = '");
      sql.append(hash_value).append("' WHERE uid = ").append(uid).append(';');
      stmt.executeUpdate(sql.toString());

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

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

    } // end finally

    // Create the message to be sent.
    String message = globalsite.getStockMessage("password-change");
    if (message==null)
    { // what? the message does not exist?
      logger.error("\"password-change\" message does not exist in config file!");
      throw new DataException("INTERNAL: password change message not specified");

    } // end if

    // Replace the message variables.
    HashMap vars = new HashMap(2);
    vars.put("username",pcr.getUserName());
    vars.put("password",new_password);
    message = StringUtil.replaceAllVariables(message,vars);

    // Find the message subject.
    String subject = globalsite.getStockMessage("password-change-subject");
    if (subject==null)
      subject = "Venice Password Changed";

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

  } // end completePasswordChange

  public UserContext createNewAccount(String remote_addr, String username, String password, String reminder,
				      java.sql.Date dob) throws DataException, AccessError
  {
    checkInitialized();
    SecurityMonitor smon = (SecurityMonitor)(globalsite.queryService(SecurityMonitor.class));
    Role new_role = smon.getDefaultRole("Global.NewUser");
    // email confirmation # is between 1000000 and 9999999
    int confirm_num = Generator.get().getNewConfirmationNumber();

    if (logger.isDebugEnabled())
      logger.debug("createNewAccount(\"" + username + "\",<password>,<reminder>)...");

    // Create the user account.
    ReturnNewUser rnu = UserContextImpl.createAccount(env,remote_addr,username,password,false,reminder,false,
						      false,confirm_num,new_role,null,dob);

    // create a new context for the user (they're now effectively logged in)
    UserContextImpl rc = new UserContextImpl(globalsite,env);
    rc.loadNewUser(remote_addr,rnu.getUserID(),new_role.getLevel(),username,confirm_num,rnu.getCreationDate(),
		   rnu.getCreationDate(),dob);
    rc.autoJoinCommunities();  // EJB 4/14/2001
    if (logger.isDebugEnabled())
      logger.debug("...created new user context");
    return rc;

  } // end createNewAccount

  public boolean aliasExists(String alias, int exist_cid)
  {
    checkInitialized();
    Connection conn = null;
    Statement stmt = null;

    try
    { // do a SELECT on the sigs table
      conn = globalsite.getConnection(null);
      stmt = conn.createStatement();
      StringBuffer sql = new StringBuffer("SELECT sigid FROM sigs WHERE alias = '");
      sql.append(alias).append("'");
      if (exist_cid>0)
	sql.append(" AND sigid <> ").append(exist_cid);
      sql.append(';');
      ResultSet rs = stmt.executeQuery(sql.toString());
      return rs.next();

    } // end try
    catch (SQLException e)
    { // force to return null
      logger.warn("DB error looking up community: " + e.getMessage(),e);

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

    } // end finally

    return false;  // this is a "default" return

  } // end aliasExists

  public boolean isValidCategoryID(int catid)
  {
    checkInitialized();
    if (catid==-1)
      return true;  // valid by definition, it means "Top"
    if (global_flags.get(BP_NOCATEGORIES))
      return (catid==0);  // EJB 1/16/2001 - if categories are disabled, the only valid one is "unclassified"
    Connection conn = null;
    Statement stmt = null;

    try
    { // do a SELECT on the category table
      conn = globalsite.getConnection(null);
      stmt = conn.createStatement();
      StringBuffer sql = new StringBuffer("SELECT catid FROM refcategory WHERE catid = ");
      sql.append(catid).append(';');
      ResultSet rs = stmt.executeQuery(sql.toString());
      return rs.next();

    } // end try
    catch (SQLException e)
    { // force to return null
      logger.warn("DB error looking up category: " + e.getMessage(),e);

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

    } // end finally

    return false;  // this is a "default" return

  } // end isValidCategoryID

  public List searchForUsers(int field, int mode, String term, int offset, int count) throws DataException
  {
    if (logger.isDebugEnabled())
      logger.debug("User search: field = " + field + ", mode = " + mode + ", term '" + term + "', offset = "
		   + offset + ", count = " + count);

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

    try
    { // get a database connection
      conn = globalsite.getConnection(null);
      stmt = conn.createStatement();
      StringBuffer sql = new StringBuffer("SELECT u.uid, u.username, u.description, c.given_name, "
					  + "c.family_name, c.locality, c.region, c.country FROM users u, "
					  + "contacts c WHERE u.contactid = c.contactid AND ");

      switch (field)
      { // determine which field to search on
	case SearchMode.FIELD_USER_NAME:
	  sql.append("u.username ");
	  break;

	case SearchMode.FIELD_USER_DESCRIPTION:
	  sql.append("u.description ");
	  break;

	case SearchMode.FIELD_USER_GIVEN_NAME:
	  sql.append("c.given_name ");
	  break;

	case SearchMode.FIELD_USER_FAMILY_NAME:
	  sql.append("c.family_name ");
	  break;

	default:
	  throw new DataException("invalid search field specified");

      } // end switch

      switch (mode)
      { // compose SQL in different ways depending on the search term
	case SearchMode.SEARCH_PREFIX:
	  sql.append("LIKE '").append(SQLUtil.encodeStringWildcards(term)).append("%'");
	  break;

	case SearchMode.SEARCH_SUBSTRING:
	  sql.append("LIKE '%").append(SQLUtil.encodeStringWildcards(term)).append("%'");
	  break;

	case SearchMode.SEARCH_REGEXP:
	  sql.append("REGEXP '").append(SQLUtil.encodeString(term)).append('\'');
	  break;

	default:
	  throw new DataException("invalid search mode specified");

      } // end switch

      sql.append(" AND u.is_anon = 0 ORDER BY u.username LIMIT ").append(offset).append(", ");
      sql.append(count+1).append(';');

      if (logger.isDebugEnabled())
	logger.debug("SQL: " + sql.toString());

      // launch the search!
      ResultSet rs = stmt.executeQuery(sql.toString());

      while (rs.next())
      { // add all the found users to the list
	UserFound uf = new UserFoundImpl(rs.getInt(1),rs.getString(2),rs.getString(3),rs.getString(4),
					 rs.getString(5),rs.getString(6),rs.getString(7),rs.getString(8));
	rc.add(uf);

      } // end while

    } // end try
    catch (SQLException e)
    { // turn SQLException into data exception
      logger.error("DB error reading user entries: " + e.getMessage(),e);
      throw new DataException("unable to retrieve 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

    return Collections.unmodifiableList(rc);

  } // end searchForUsers

  public int getSearchUserCount(int field, int mode, String term) throws DataException
  {
    if (logger.isDebugEnabled())
      logger.debug("User search: field = " + field + ", mode = " + mode + ", term '" + term + "'");

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

    try
    { // get a database connection
      conn = globalsite.getConnection(null);
      stmt = conn.createStatement();
      StringBuffer sql = new StringBuffer("SELECT COUNT(*) FROM users u, contacts c WHERE u.contactid = "
					  + "c.contactid AND ");

      switch (field)
      { // determine which field to search on
	case SearchMode.FIELD_USER_NAME:
	  sql.append("u.username ");
	  break;

	case SearchMode.FIELD_USER_DESCRIPTION:
	  sql.append("u.description ");
	  break;

	case SearchMode.FIELD_USER_GIVEN_NAME:
	  sql.append("c.given_name ");
	  break;

	case SearchMode.FIELD_USER_FAMILY_NAME:
	  sql.append("c.family_name ");
	  break;

	default:
	  throw new DataException("invalid search field specified");

      } // end switch

      switch (mode)
      { // compose SQL in different ways depending on the search term
	case SearchMode.SEARCH_PREFIX:
	  sql.append("LIKE '").append(SQLUtil.encodeStringWildcards(term)).append("%'");
	  break;

	case SearchMode.SEARCH_SUBSTRING:
	  sql.append("LIKE '%").append(SQLUtil.encodeStringWildcards(term)).append("%'");
	  break;

	case SearchMode.SEARCH_REGEXP:
	  sql.append("REGEXP '").append(SQLUtil.encodeString(term)).append('\'');
	  break;

	default:
	  throw new DataException("invalid search mode specified");

      } // end switch

      sql.append(" AND u.is_anon = 0;");


      if (logger.isDebugEnabled())
	logger.debug("SQL: " + sql.toString());

      // launch the search!
      ResultSet rs = stmt.executeQuery(sql.toString());

      if (!(rs.next()))
	throw new InternalStateError("getSearchUserCount search failure (MUST have 1 row!)");

      return rs.getInt(1);

    } // end try
    catch (SQLException e)
    { // turn SQLException into data exception
      logger.error("DB error reading user entries: " + e.getMessage(),e);
      throw new DataException("unable to retrieve 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 getSearchUserCount

  public int getStdNumSearchResults()
  {
    return gp_ints[IP_MAXSEARCHRETURN];

  } // end getStdNumSearchResults

  public boolean isEmailAddressBanned(String email)
  {
    Connection conn = null;      // pooled database connection
    Statement stmt = null;

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

      // create and execute the right SQL statement
      StringBuffer sql = new StringBuffer("SELECT by_uid FROM emailban WHERE address = '");
      sql.append(SQLUtil.encodeString(email)).append("';");
      ResultSet rs = stmt.executeQuery(sql.toString());
      return rs.next();

    } // end try
    catch (SQLException e)
    { // if the database is horked, return false
      logger.warn("DB error looking email address: " + 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;

  } // end isEmailAddressBanned

  public boolean confAliasExists(String alias)
  {
    checkInitialized();
    Connection conn = null;
    Statement stmt = null;

    try
    { // do a SELECT on the confalias table
      conn = globalsite.getConnection(null);
      stmt = conn.createStatement();
      StringBuffer sql = new StringBuffer("SELECT confid FROM confalias WHERE alias = '");
      sql.append(alias).append("';");
      ResultSet rs = stmt.executeQuery(sql.toString());
      return rs.next();

    } // end try
    catch (SQLException e)
    { // force to return false
      logger.warn("DB error looking up conference: " + e.getMessage(),e);

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

    } // end finally

    return false;  // this is a "default" return

  } // end confAliasExists

  public HTMLChecker getEscapingChecker()
  {
    return (HTMLChecker)(globalsite.queryService(HTMLChecker.class,"escaper"));

  } // end getEscapingChecker

  public int getNumPostsPerPage()
  {
    return gp_ints[IP_POSTSPERPAGE];

  } // end getNumPostsPerPage

  public int getNumOldPostsBeforeNew()
  {
    return gp_ints[IP_POSTSATTOP];

  } // end getNumOldPostsBeforeNew

  public int getMaxNumConferenceMembersDisplay()
  {
    return gp_ints[IP_MAXCONFMEMBERDISPLAY];

  } // end getMaxNumConferenceMembersDisplay

  public int getMaxNumCommunityMembersDisplay()
  {
    return gp_ints[IP_MAXCOMMUNITYMEMBERDISPLAY];

  } // end getMaxNumCommunityMembersDisplay

  public List getMasterSideBoxList()
  {
    return Arrays.asList(sideboxes);

  } // end getMasterSideBoxList

  public List getPublishedMessages(boolean all) throws DataException
  {
    ArrayList rc = new ArrayList();

    synchronized (this)
    { // Make sure the cache is in condition.
      while (cache_fp_posts_busy)
      { // the list is busy, don't fsck with it until we're done
	try
	{ // wait to see if they can be un-busy
	  wait();

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

      } // end while

      // If the cache list contains too few items, backfill it from the database.
      if (cache_fp_posts.size()<gp_ints[IP_NUMFRONTPAGEPOSTS])
	PublishedMessageImpl.backfillCache(cache_fp_posts,gp_ints[IP_NUMFRONTPAGEPOSTS],env);

      // Copy the contents to the return vector.
      Iterator it = cache_fp_posts.iterator();
      while (it.hasNext())
      { // cast each element and add the casted object to the output
	TopicMessageContext ctxt = (TopicMessageContext)(it.next());
	rc.add(ctxt);

      } // end while

    } // end synchronized block

    if (all)  // add the extra postings to the list
      PublishedMessageImpl.backfillReturn(rc,env);

    return Collections.unmodifiableList(rc);

  } // end getPublishedMessages

  public int getNumAuditRecordsPerPage()
  {
    return gp_ints[IP_NUMAUDITRECSPERPAGE];

  } // end getNumAuditRecordsPerPage

  public Advertisement getAdByID(int id)
  {
    return AdvertisementImpl.getAdByID(env,id);

  } // end getAdByID

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

  } // end selectAd

  public BinaryData loadImage(int id) throws DataException
  {
    return ImageStore.loadImageByID(env,id);

  } // end loadImage

  public Dimension getUserPhotoSize()
  {
    return DEFAULT_DIM_USERPHOTO;

  } // end getUserPhotoSize

  public Dimension getCommunityLogoSize()
  {
    return DEFAULT_DIM_COMMUNITYLOGO;

  } // end getCommunityLogoSize

  public SecurityInfo getSecurityInfo()
  {
    SecurityMonitor smon = (SecurityMonitor)(globalsite.queryService(SecurityMonitor.class));
    return new SecurityInfoWrapper(smon);

  } // end getSecurityInfo

  public boolean useCategories()
  {
    return !(global_flags.get(BP_NOCATEGORIES));

  } // end useCategories

  public String testIPBan(String ip_address) throws DataException
  {
    checkInitialized();
    long addy = 0;
    try
    { // convert the IP address parameter to a long integer
      addy = IPv4Util.stringToLongAddress(ip_address);

    } // end try
    catch (IllegalArgumentException e)
    { // wrap the exception and return it
      throw new DataException(e);

    } // end catch

    Connection conn = null;
    PreparedStatement stmt = null;
    ResultSet rs = null;

    try
    { // Look for an IP ban that (a) matches this IP address (by mask), (b) has not yet expired (or does not
      // expire), and (c) is enabled.
      conn = globalsite.getConnection(null);
      stmt = conn.prepareStatement("SELECT message FROM ipban WHERE (address & mask) = (? & mask) "
				   + "AND (expire IS NULL OR expire >= ?) AND enable <> 0 ORDER BY mask DESC "
				   + "LIMIT 1;");
      stmt.setLong(1,addy);
      stmt.setString(2,SQLUtil.encodeDate(new java.util.Date()));
      rs = stmt.executeQuery();
      if (rs.next())  // located an IP ban
	return rs.getString(1);

    } // end try
    catch (SQLException e)
    { // bug out
      throw new DataException("Error accessing IP ban database: " + e.getMessage(),e);

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

    } // end finally

    return null;  // no IP ban found

  } // end testIPBan

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

  /**
   * Queries this object for a specified service.
   *
   * @param klass The class of the object that should be returned as a service.
   * @return A service object.  The service object is guaranteed to be of the class
   *         specified by <CODE>klass</CODE>; that is, if <CODE>queryService(klass)</CODE>
   *         yields some object <CODE>x</CODE>, then the expression <CODE>klass.isInstance(x)</CODE>
   *         is true.
   * @exception com.silverwrist.venice.except.NoSuchServiceException If no service is available in
   *            the specified class.
   */
  public Object queryService(Class klass)
  {
    if (klass==VeniceEngine.class)
      return ((VeniceEngine)this);
    if (klass==EngineBackend.class)
      return ((EngineBackend)this);
    throw new NoSuchServiceException("VeniceEngine",klass);

  } // end queryService

  /**
   * Queries this object for a specified service.
   *
   * @param klass The class of the object that should be returned as a service.
   * @param serviceid ID for the service to be requested, to further discriminate between requests.
   * @return A service object.  The service object is guaranteed to be of the class
   *         specified by <CODE>klass</CODE>; that is, if <CODE>queryService(klass)</CODE>
   *         yields some object <CODE>x</CODE>, then the expression <CODE>klass.isInstance(x)</CODE>
   *         is true.
   * @exception com.silverwrist.venice.except.NoSuchServiceException If no service is available in
   *            the specified class.
   */
  public Object queryService(Class klass, String serviceid)
  {
    return queryService(klass);  // defer to no service ID

  } // end queryService

  /*--------------------------------------------------------------------------------
   * Implementations from interface EngineBackend
   *--------------------------------------------------------------------------------
   */

  public CommunityData getCommunityDataObject(int cid) throws DataException
  {
    checkInitialized();
    if (logger.isDebugEnabled())
      logger.debug("getCommunityDataObject(" + cid + ")...");

    try
    { // delegate to the comm_objcache object
      return (CommunityData)(comm_objcache.getOrCreate(new Integer(cid)));

    } // end try
    catch (ObjectFactoryException e)
    { // this may be a DataException, or it may not
      Throwable e2 = e.getException();
      if (e2 instanceof DataException)
	throw (DataException)e2;
      else
	throw new InternalStateError("unknown creation exception thrown in getCommunityDataObject: "
				     + e2.getClass().getName(),e2);

    } // end catch

  } // end getCommunityDataObject

  public void detachCommunityDataObject(int cid)
  {
    checkInitialized();
    if (logger.isDebugEnabled())
      logger.debug("detachCommunityDataObject(" + cid + ")...");

    comm_objcache.detach(new Integer(cid));

  } // end detachCommunityDataObject

  public void registerNewCommunity(CommunityData comm)
  {
    checkInitialized();
    if (logger.isDebugEnabled())
      logger.debug("registerNewCommunity(" + comm.getID() + ")...");

    comm_objcache.register(new Integer(comm.getID()),comm);

  } // end registerNewCommunity

  public ConferenceData getConferenceDataObject(int confid) throws DataException
  {
    checkInitialized();
    if (logger.isDebugEnabled())
      logger.debug("getConferenceDataObject(" + confid + ")...");

    try
    { // delegate to the conf_objcache objects
      return (ConferenceData)(conf_objcache.getOrCreate(new Integer(confid)));

    } // end try
    catch (ObjectFactoryException e)
    { // this may be a DataException, or it may not
      Throwable e2 = e.getException();
      if (e2 instanceof DataException)
	throw (DataException)e2;
      else
	throw new InternalStateError("unknown creation exception thrown in getConferenceDataObject: "
				     + e2.getClass().getName(),e2);

    } // end catch

  } // end getConferenceDataObject

  public void detachConferenceDataObject(int confid)
  {
    checkInitialized();
    if (logger.isDebugEnabled())
      logger.debug("detachConferenceDataObject(" + confid + ")...");

    conf_objcache.detach(new Integer(confid));

  } // end detachConferenceDataObject

  public void registerNewConference(ConferenceData conf)
  {
    checkInitialized();
    if (logger.isDebugEnabled())
      logger.debug("registerNewConference(" + conf.getID() + ")...");

    conf_objcache.register(new Integer(conf.getID()),conf);

  } // end registerNewConference

  public int getParamInt(int selector)
  {
    return gp_ints[selector];

  } // end getParamInt

  public boolean getParamBoolean(int selector)
  {
    return global_flags.get(selector);

  } // end getParamBoolean

  public Role getParamRole(int selector)
  {
    if (selector==ROLEP_CREATECOMMUNITY)
    { // resolve with respect to the global security monitor
      SecurityMonitor smon = (SecurityMonitor)(globalsite.queryService(SecurityMonitor.class));
      return smon.getRoleForLevel(gp_ints[IP_CREATECOMMUNITYLVL]);

    } // end if

    return null;

  } // end getParamRole

  public void forceParamReload() throws DataException
  {
    Connection conn = null;  // data pooled connection
    Statement stmt = null;

    try
    { // get a connection and use it to reload
      conn = globalsite.getConnection(null);
      stmt = conn.createStatement();
      loadDefaults(stmt);

    } // end try
    catch (SQLException e)
    { // just log an error if we screwed up
      logger.error("DB error loading parameters: " + e.getMessage(),e);
      throw new DataException("Database error reloading parameters: " + e.getMessage(),e);

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

    } // end finally

  } // end forceParamReload

  public SideBoxDescriptor getMasterSideBoxDescriptor(int id)
  {
    return (SideBoxDescriptor)(sidebox_ids.get(new Integer(id)));

  } // end getMasterSideBoxDescriptor

  public synchronized void startPublish()
  {
    cache_fp_posts_busy = true;

  } // end startPublish

  public synchronized void publishNew(TopicMessageContext pubmsg)
  {
    if (pubmsg!=null)
    { // add the new message
      cache_fp_posts.addFirst(pubmsg);
      while (cache_fp_posts.size()>gp_ints[IP_NUMFRONTPAGEPOSTS])
	cache_fp_posts.removeLast();

    } // end pubmsg

    cache_fp_posts_busy = false;
    notifyAll();

  } // end publishNew

  public synchronized void unpublish(long postid)
  {
    Iterator it = cache_fp_posts.iterator();
    while (it.hasNext())
    { // get each published message in the cache in turn, looking for the specified post ID
      TopicMessageContext tmc = (TopicMessageContext)(it.next());
      if (tmc.getPostID()==postid)
      { // drop the specified post ID like a hot rock
	it.remove();
	break;

      } // end if

    } // end while

  } // end unpublish

  public boolean isNoCompressMimeType(String type)
  {
    if (no_compress_types.isEmpty())
      return false;  // can't possibly be no-compress

    // strip the MIME type name down to basics
    int psem = type.indexOf(';');
    String real_type;
    if (psem>=0)
      real_type = type.substring(0,psem).trim().toLowerCase();
    else
      real_type = type.trim().toLowerCase();

    return no_compress_types.contains(real_type);

  } // end isNoCompressMimeType

  public GlobalProperties getProperties()
  {
    return createProperties();

  } // end getProperties

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

    boolean succeeded = false;
    try
    { // write the changes out to the database
      updateDefaults(delta);
      succeeded = true;

    } // end try
    finally
    { // restore old properties on failure
      if (!succeeded)
	storeProperties(orig);

    } // end finally

  } // end setProperties

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

  static
  { // Initialize the "integer parameter properties" BitSet.
    BitSet tmp = new BitSet();
    tmp.set(_PPROP_IP_BASE + IP_POSTSPERPAGE);
    tmp.set(_PPROP_IP_BASE + IP_POSTSATTOP);
    tmp.set(_PPROP_IP_BASE + IP_MAXSEARCHRETURN);
    tmp.set(_PPROP_IP_BASE + IP_MAXCOMMUNITYMEMBERDISPLAY);
    tmp.set(_PPROP_IP_BASE + IP_MAXCONFMEMBERDISPLAY);
    tmp.set(_PPROP_IP_BASE + IP_NUMFRONTPAGEPOSTS);
    tmp.set(_PPROP_IP_BASE + IP_NUMAUDITRECSPERPAGE);
    tmp.set(_PPROP_IP_BASE + IP_CREATECOMMUNITYLVL);
    IP_PROPS = tmp;

    // Initialize the "all properties" BitSet.
    tmp = new BitSet();
    tmp.set(PROP_FLAGS);
    tmp.or(IP_PROPS);
    ALL_PROPS = tmp;

  } // end static initializer

} // end class VeniceEngineImpl
