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

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

class TopicMessageUserContextImpl implements TopicMessageContext
{
  /*--------------------------------------------------------------------------------
   * Static data members
   *--------------------------------------------------------------------------------
   */

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

  private static final int MAX_ATTACH = 1048576;  // TODO: should be a configurable parameter

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

  private EnvConference env;                   // the conference environment
  private long postid;                         // the ID of this post
  private long parent;                         // the parent post ID (not really used)
  private int topicid;                         // the topic ID we live within
  private int num;                             // the post number within the topic
  private int linecount;                       // number of lines in message
  private int creator_uid;                     // the UID of the poster
  private java.util.Date posted;               // date and time posted
  private boolean hidden;                      // has this post been hidden?
  private int scribble_uid;                    // UID of the user who scribbled it
  private java.util.Date scribble_date;        // date and time it was scribbled
  private String pseud;                        // the pseud attached to this message
  private int datalen;                         // length in bytes of attachment
  private String filename;                     // file name of attachment
  private String mimetype;                     // MIME type for attachment
  private int stgmethod;                       // storage method for attachment
  private boolean nuked = false;               // has this message been nuked?
  private String creator_cache = null;         // cache for username of creator
  private String text_cache = null;            // cache for actual message text

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

  protected TopicMessageUserContextImpl(EnvConference env, long postid, long parent, int topicid, int num,
					int linecount, int creator_uid, java.util.Date posted, boolean hidden,
					int scribble_uid, java.util.Date scribble_date, String pseud,
					int datalen, String filename, String mimetype, int stgmethod)
  {
    this.env = env;
    this.postid = postid;
    this.parent = parent;
    this.topicid = topicid;
    this.num = num;
    this.linecount = linecount;
    this.creator_uid = creator_uid;
    this.posted = posted;
    this.hidden = hidden;
    this.scribble_uid = scribble_uid;
    this.scribble_date = scribble_date;
    this.pseud = pseud;
    this.datalen = datalen;
    this.filename = filename;
    this.mimetype = mimetype;
    this.stgmethod = stgmethod;

  } // end constructor

  TopicMessageUserContextImpl(EnvConference env, long postid, long parent, int topicid, int num, int linecount,
			      int creator_uid, java.util.Date posted, String pseud)
  {
    this.env = env;
    this.postid = postid;
    this.parent = parent;
    this.topicid = topicid;
    this.num = num;
    this.linecount = linecount;
    this.creator_uid = creator_uid;
    this.posted = posted;
    this.hidden = false;
    this.scribble_uid = 0;
    this.scribble_date = null;
    this.pseud = pseud;
    this.datalen = 0;
    this.filename = null;
    this.mimetype = null;
    this.stgmethod = -1;

  } // end constructor

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

  private static final String quickGetUserName(Connection conn, int uid) throws SQLException
  {
    Statement stmt = null;

    try
    { // do a quick search
      stmt = conn.createStatement();
      ResultSet rs = stmt.executeQuery("SELECT username FROM users WHERE uid = " + uid + ";");
      if (rs.next())
	return rs.getString(1);
      else
	return "(unknown)";

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

    } // end finally

  } // end quickGetUserName

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

    try
    { // run against the database
      stmt = conn.createStatement();
      StringBuffer sql = new StringBuffer("SELECT posts.hidden, posts.scribble_uid, posts.scribble_date, "
					  + "posts.pseud, postattach.datalen, postattach.filename, "
					  + "postattach.mimetype, postattach.stgmethod FROM posts "
					  + "LEFT JOIN postattach ON posts.postid = postattach.postid "
					  + "WHERE posts.postid = ");
      sql.append(postid).append(';');
      ResultSet rs = stmt.executeQuery(sql.toString());
      if (rs.next())
      { // update a variety of fields
	hidden = rs.getBoolean(1);
	scribble_uid = rs.getInt(2);
	scribble_date = SQLUtil.getFullDateTime(rs,3);
	pseud = rs.getString(4);
	datalen = rs.getInt(5);
	filename = rs.getString(6);
	mimetype = rs.getString(7);
	stgmethod = rs.getInt(8);
	if (rs.wasNull())
	  stgmethod = -1;

      } // end if
      else
      { // the post has been nuked - update accordingly
	linecount = 0;
	creator_uid = -1;
	posted = null;
	hidden = false;
	scribble_uid = -1;
	scribble_date = null;
	pseud = null;
	datalen = 0;
	filename = null;
	mimetype = null;
	stgmethod = -1;
	nuked = true;
	creator_cache = null;
	text_cache = null;

      } // end else

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

    } // end finally

  } // end refresh

  /*--------------------------------------------------------------------------------
   * Semi-internal operations
   *--------------------------------------------------------------------------------
   */

  final String getScribblingUser() throws DataException
  {
    if (scribble_uid==-1)
      return null;

    Connection conn = null;
    try
    { // use a database connection to get the user name
      conn = env.getConnection();
      refresh(conn);
      if (nuked)
	return null;  // post nuked!
      return quickGetUserName(conn,scribble_uid);

    } // end try
    catch (SQLException e)
    { // turn this into a DataException
      logger.error("DB error reading user name: " + e.getMessage(),e);
      throw new DataException("unable to retrieve user name: " + e.getMessage(),e);

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

    } // end finally

  } // end getScribblingUser

  /*--------------------------------------------------------------------------------
   * Implementations from interface TopicMessageContext
   *--------------------------------------------------------------------------------
   */

  public long getPostID()
  {
    return postid;

  } // end getPostID

  public long getParentPostID()
  {
    return parent;

  } // end getParentPostID

  public int getPostNumber()
  {
    return num;

  } // end getPostNumber

  public int getNumLines()
  {
    return linecount;

  } // end getNumLines

  public int getCreatorUID()
  {
    return creator_uid;

  } // end getCreatorUID

  public String getCreatorName() throws DataException
  {
    if (creator_cache==null)
    { // we don't have the user name yet, get it out of the database
      if (nuked)
	return null;  // post nuked!
      Connection conn = null;

      try
      { // use a database connection to get the user name
	conn = env.getConnection();
	refresh(conn);
	if (nuked)
	  return null;  // post nuked!
	creator_cache = quickGetUserName(conn,creator_uid);

      } // end try
      catch (SQLException e)
      { // turn this into a DataException
	logger.error("DB error reading user name: " + e.getMessage(),e);
	throw new DataException("unable to retrieve user name: " + e.getMessage(),e);

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

      } // end finally

    } // end if

    return creator_cache;

  } // end getCreatorName

  public java.util.Date getPostDate()
  {
    return posted;

  } // end getPostDate

  public boolean isHidden()
  {
    return hidden;

  } // end isHidden

  public boolean isScribbled()
  {
    return (scribble_date!=null);

  } // end isScribbled

  public boolean isNuked()
  {
    return nuked;

  } // end isNuked

  public java.util.Date getScribbleDate()
  {
    return scribble_date;

  } // end getScribbleDate

  public String getPseud()
  {
    return pseud;

  } // return pseud

  public String getBodyText() throws DataException
  {
    if (text_cache==null)
    { // we don't have the body text yet, go get it
      Connection conn = null;
      if (nuked)
	return null;  // post nuked!

      try
      { // use a database connection to get the body text
	conn = env.getConnection();
	refresh(conn);

	if (nuked)
	  return null;  // post nuked!

	if (scribble_date==null)
	{ // let's go get the body text!
	  Statement stmt = null;

	  try
	  { // retrieve the body text
	    stmt = conn.createStatement();
	    ResultSet rs = stmt.executeQuery("SELECT data FROM postdata WHERE postid = " + postid + ";");
	    if (rs.next())
	      text_cache = rs.getString(1);
	    else
	      return "<b>[[Venice Internal Error: Data Missing]]</b>";
	      // FUTURE: throw an exception?

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

	  } // end finally

	} // end if
	else // for scribbled posts, we return the scribbler's name only
	  text_cache = quickGetUserName(conn,scribble_uid);

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

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

      } // end finally

    } // end if

    return text_cache;

  } // end getBodyText

  public boolean hasAttachment()
  {
    return (mimetype!=null);

  } // end hasAttachment

  public String getAttachmentType()
  {
    return mimetype;

  } // end getAttachmentType

  public String getAttachmentFilename()
  {
    return ((mimetype!=null) ? filename : null);

  } // end getAttachmentFilename

  public int getAttachmentLength()
  {
    return ((mimetype!=null) ? datalen : 0);

  } // end getAttachmentLength

  public InputStream getAttachmentData() throws AccessError, DataException
  {
    if (logger.isDebugEnabled())
      logger.debug("getAttachmentData() for post " + postid);
    if (nuked || (scribble_date!=null))
    { // this would be an exercise in futility!
      logger.error("cannot attach to a nuked or scribbled message");
      throw new AccessError("You cannot attach data to a message that no longer exists.");

    } // end if

    Connection conn = null;
    Statement stmt = null;
    ResultSet rs = null;
    InputStream rc = null;
    try
    { // open up a database connection
      conn = env.getConnection();

      // make sure we have current data
      refresh(conn);
      if (nuked || (scribble_date!=null))
      { // this would be an exercise in futility!
	logger.error("cannot attach to a nuked or scribbled message");
	throw new AccessError("You cannot attach data to a message that no longer exists.");

      } // end if

      if (mimetype==null)
      { // there is no attachment data!
	logger.error("no attachment data to get");
	throw new AccessError("There is no attachment data for this message.");

      } // end if

      // This will cause a "hit" on the attachment data.  Update that record.
      stmt = conn.createStatement();
      StringBuffer sql = new StringBuffer("UPDATE postattach SET hits = hits + 1, last_hit = '");
      sql.append(SQLUtil.encodeDate(new java.util.Date())).append("' WHERE postid = ").append(postid);
      sql.append(';');
      stmt.executeUpdate(sql.toString());

      // Create the SQL we need to retrieve the attachment.
      sql.setLength(0);
      sql.append("SELECT data FROM postattach WHERE postid = ").append(postid).append(';');

      // Execute the query!
      rs = stmt.executeQuery(sql.toString());
      if (!(rs.next()))
      { // there is no attachment data!
	logger.error("no attachment data to get");
	throw new AccessError("There is no attachment data for this message.");

      } // end if

      // Since the InputStream we get from JDBC will probably go away when the connection is dropped, we
      // need to make a temporary copy of it, to a ByteArrayOutputStream.  (Attachments can only be
      // 1 Mb in size, so this shouldn't be a big problem.)
      InputStream sqldata = rs.getBinaryStream(1);
      InputStream real_input;
      switch (stgmethod)
      { // where's our input really coming from
	case 0:   // copy verbatim
	  logger.debug("stgmethod 0 = straight copy");
	  real_input = sqldata;
	  break;

	case 1:   // gunzip it first
	  logger.debug("stgmethod 1 = GZIP decompress");
	  real_input = new GZIPInputStream(sqldata);
	  break;

	default:
	  throw new DataException("Unknown storage method value: " + stgmethod);

      } // end switch

      // Copy to a new stream.
      byte[] ary = IOUtil.load(real_input);
      if (logger.isDebugEnabled())
	logger.debug("loaded data of length " + ary.length);
      rc = new ByteArrayInputStream(ary);

    } // end try
    catch (SQLException e)
    { // turn this into a DataException
      logger.error("DB error retrieving attachment: " + e.getMessage(),e);
      throw new DataException("unable to retrieve attachment data: " + e.getMessage(),e);

    } // end catch
    catch (IOException e)
    { // turn this into a DataException too
      logger.error("I/O error copying attachment data: " + e.getMessage(),e);
      throw new DataException("unable to retrieve attachment data: " + e.getMessage(),e);

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

    } // end finally

    return rc;

  } // end getAttachmentData

  public int getAttachmentData(OutputStream here) throws AccessError, DataException, IOException
  {
    if (logger.isDebugEnabled())
      logger.debug("getAttachmentData() for post " + postid);
    if (nuked || (scribble_date!=null))
    { // this would be an exercise in futility!
      logger.error("cannot attach to a nuked or scribbled message");
      throw new AccessError("You cannot attach data to a message that no longer exists.");

    } // end if

    Connection conn = null;
    Statement stmt = null;
    ResultSet rs = null;
    try
    { // open up a database connection
      conn = env.getConnection();

      // make sure we have current data
      refresh(conn);
      if (nuked || (scribble_date!=null))
      { // this would be an exercise in futility!
	logger.error("cannot attach to a nuked or scribbled message");
	throw new AccessError("You cannot attach data to a message that no longer exists.");

      } // end if

      if (mimetype==null)
      { // there is no attachment data!
	logger.error("no attachment data to get");
	throw new AccessError("There is no attachment data for this message.");

      } // end if

      // This will cause a "hit" on the attachment data.  Update that record.
      stmt = conn.createStatement();
      StringBuffer sql = new StringBuffer("UPDATE postattach SET hits = hits + 1, last_hit = '");
      sql.append(SQLUtil.encodeDate(new java.util.Date())).append("' WHERE postid = ").append(postid);
      sql.append(';');
      stmt.executeUpdate(sql.toString());

      // Create the SQL we need to retrieve the attachment.
      sql.setLength(0);
      sql.append("SELECT data FROM postattach WHERE postid = ").append(postid).append(';');

      // Execute the query!
      rs = stmt.executeQuery(sql.toString());
      if (!(rs.next()))
      { // there is no attachment data!
	logger.error("no attachment data to get");
	throw new AccessError("There is no attachment data for this message.");

      } // end if

      // Since the InputStream we get from JDBC will probably go away when the connection is dropped, we
      // need to make a temporary copy of it, to a ByteArrayOutputStream.  (Attachments can only be
      // 1 Mb in size, so this shouldn't be a big problem.)
      InputStream sqldata = rs.getBinaryStream(1);
      InputStream real_input;
      switch (stgmethod)
      { // where's our input really coming from
	case 0:   // copy verbatim
	  logger.debug("stgmethod 0 = straight copy");
	  real_input = sqldata;
	  break;

	case 1:   // gunzip it first
	  logger.debug("stgmethod 1 = GZIP decompress");
	  real_input = new GZIPInputStream(sqldata);
	  break;

	default:
	  throw new DataException("Unknown storage method value: " + stgmethod);

      } // end switch

      // copy the data and return number of bytes copied
      return IOUtil.copy(real_input,here);

    } // end try
    catch (SQLException e)
    { // turn this into a DataException
      logger.error("DB error retrieving attachment: " + e.getMessage(),e);
      throw new DataException("unable to retrieve attachment data: " + e.getMessage(),e);

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

    } // end finally

  } // end getAttachmentData

  public boolean canHide()
  {
    return (   ((creator_uid==env.getUserID()) && (!env.isAnonymous()))
	    || env.getConference().userCanHide());

  } // end canHide

  public boolean canScribble()
  {
    return (   ((creator_uid==env.getUserID()) && (!env.isAnonymous()))
	    || env.getConference().userCanScribble());

  } // end canScribble

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

  } // end canNuke

  public void setHidden(boolean flag) throws DataException, AccessError
  {
    if (env.isAnonymous())
      return;  // no-op
    if ((creator_uid!=env.getUserID()) && !(env.getConference().userCanHide()))
    { // we can't change the hidden status!
      logger.error("trying to set hidden status of post w/o permission!");
      throw new AccessError("You are not permitted to change the hidden status of this message.");

    } // end if

    if (nuked || (scribble_date!=null) || (hidden==flag))
      return;  // changing the status of a nuked or scribbled post is futile

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

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

      // lock the tables we reference
      stmt.executeUpdate("LOCK TABLES posts WRITE, postattach READ;");
      try
      { // first, make sure we have the right status for our post
	refresh(conn);
	if (nuked || (scribble_date!=null))
	  return;  // changing the status of a nuked or scribbled post is futile
	if (hidden==flag)
	  return;  // this is a no-op

	// update the "hidden" flag in the database
	StringBuffer sql = new StringBuffer("UPDATE posts SET hidden = ");
	sql.append(flag ? '1' : '0').append(" WHERE postid = ").append(postid).append(';');
	stmt.executeUpdate(sql.toString());

	hidden = flag;  // store flag

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

      } // end finally

      // record what we did in an audit record
      ar = env.newAudit(AuditRecord.HIDE_MESSAGE,"conf=" + env.getConfID() + ",post=" + postid,
			flag ? "hide" : "unhide");
      
    } // end try
    catch (SQLException e)
    { // turn this into a DataException
      logger.error("DB error setting hidden status: " + e.getMessage(),e);
      throw new DataException("unable to set hidden status: " + e.getMessage(),e);

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

    } // end finally

  } // end setHidden

  public void scribble() throws DataException, AccessError
  {
    if (env.isAnonymous())
      return;  // no-op
    if ((creator_uid!=env.getUserID()) && !(env.getConference().userCanScribble()))
    { // we can't scribble this post
      logger.error("trying to scribble post w/o permission!");
      throw new AccessError("You are not permitted to scribble this message.");

    } // end if

    if (nuked || (scribble_date!=null))
      return;  // scribbling a nuked or scribbled post is futile

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

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

      // lock the tables we reference
      stmt.executeUpdate("LOCK TABLES posts WRITE, postdata WRITE, postattach WRITE, postpublish WRITE;");
      try
      { // first, make sure we have the right status for our post
	refresh(conn);
	if (nuked || (scribble_date!=null))
	  return;  // scribbling a nuked or scribbled post is futile

	// First, set the appropriate "scribbled" information in the "header".
	StringBuffer sql = new StringBuffer("UPDATE posts SET linecount = 0, hidden = 0, scribble_uid = ");
	sql.append(env.getUserID()).append(", scribble_date = '");
	java.util.Date now = new java.util.Date();
	final String scribble_pseud = "<EM><B>(Scribbled)</B></EM>";  // TODO: configurable option
	sql.append(SQLUtil.encodeDate(now)).append("', pseud = '").append(scribble_pseud);
	sql.append("' WHERE postid = ").append(postid).append(';');
	if (logger.isDebugEnabled())
	  logger.debug("SQL: " + sql.toString());
	stmt.executeUpdate(sql.toString());

	// Determine if we need to "rub out" the post before we delete it.
	ResultSet rs = stmt.executeQuery("SELECT LENGTH(data) FROM postdata WHERE postid = " + postid + ";");
	if (rs.next())
	{ // use this data to overwrite the post with X's
	  int len = rs.getInt(1);
	  if (len>0)
	  { // construct the "rubout" statement and execute it
	    sql.setLength(0);
	    sql.append("UPDATE postdata SET data = '");
	    while (len>0)
	    { // generate a string of X's the length of the post
	      sql.append('X');
	      len--;

	    } // end while

	    sql.append("' WHERE postid = ").append(postid).append(';');
	    stmt.executeUpdate(sql.toString());

	  } // end if
	  // else not much need to do a rubout

	} // end if
	// else don't try...we're deleting the row anyway

	SQLUtil.shutdown(rs);

	// Delete the actual post data row.
	sql.setLength(0);
	sql.append("DELETE FROM postdata WHERE postid = ").append(postid).append(';');
	stmt.executeUpdate(sql.toString());

	// Delete the attachment data row.
	// FUTURE: can we do an overwrite on the attachment the way we did on the post data?
	sql.setLength(0);
	sql.append("DELETE FROM postattach WHERE postid = ").append(postid).append(';');
	stmt.executeUpdate(sql.toString());

	// Un-publish the posting.
	sql.setLength(0);
	sql.append("DELETE FROM postpublish WHERE postid = ").append(postid).append(';');
	if (stmt.executeUpdate(sql.toString())>0)
	  env.getEngine().unpublish(postid);

	// Update our internal data fields.
	linecount = 0;
	hidden = false;
	scribble_uid = env.getUserID();
	scribble_date = now;
	pseud = scribble_pseud;
	text_cache = null;
	
      } // end try
      finally
      { // unlock the tables before we go
	SQLUtil.unlockTables(conn);

      } // end finally

      // record what we did in an audit record
      ar = env.newAudit(AuditRecord.SCRIBBLE_MESSAGE,"conf=" + env.getConfID() + ",post=" + postid);
      
    } // end try
    catch (SQLException e)
    { // turn this into a DataException
      logger.error("DB error scribbling post: " + e.getMessage(),e);
      throw new DataException("unable to scribble message: " + e.getMessage(),e);

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

    } // end finally

  } // end scribble

  public void nuke() throws DataException, AccessError
  {
    if (!(env.getConference().userCanNuke()))
    { // we can't scribble this post
      logger.error("trying to nuke post w/o permission!");
      throw new AccessError("You are not permitted to nuke this message.");

    } // end if

    if (nuked)
      return;  // nuking a nuked post is futile

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

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

      // lock the tables we reference
      stmt.executeUpdate("LOCK TABLES posts WRITE, postdata WRITE, postattach WRITE, postdogear WRITE, "
			 + "postpublish WRITE, topics WRITE;");

      try
      { // first, make sure we have the right status for our post
	refresh(conn);
	if (nuked)
	  return;  // nuking a nuked post is futile

	// We need the post's topic ID and post number to renumber the topic afterwards, so grab 'em.
	ResultSet rs = stmt.executeQuery("SELECT topicid, num FROM posts WHERE postid = " + postid + ";");
	if (!(rs.next()))
	  throw new InternalStateError("lost the post to be nuked after refresh?");
	int x_topic_id = rs.getInt(1);
	int x_post_num = rs.getInt(2);
	SQLUtil.shutdown(rs);

	// Delete any and all references to this post!
	stmt.executeUpdate("DELETE FROM posts WHERE postid = " + postid + ";");
	stmt.executeUpdate("DELETE FROM postdata WHERE postid = " + postid + ";");
	stmt.executeUpdate("DELETE FROM postattach WHERE postid = " + postid + ";");
	stmt.executeUpdate("DELETE FROM postdogear WHERE postid = " + postid + ";");
	if (stmt.executeUpdate("DELETE FROM postpublish WHERE postid = " + postid + ";")>0)
	  env.getEngine().unpublish(postid);

	// Phase 1 of renumber - Renumber all posts IN THIS TOPIC that had a number higher than the one
	// we just blew to hell.  (Must have the "topicid =" clause in there - see Bug #415473)
	stmt.executeUpdate("UPDATE posts SET num = (num - 1) WHERE topicid = " + x_topic_id + " AND num > "
			   + x_post_num + ";");

	// Phase 2 of renumber - The topic now has one less post in it.  Reflect that.
	stmt.executeUpdate("UPDATE topics SET top_message = (top_message - 1) WHERE topicid = "
			   + x_topic_id + ";");

	// Update our internal variables.
	linecount = 0;
	creator_uid = -1;
	posted = null;
	hidden = false;
	scribble_uid = -1;
	scribble_date = null;
	pseud = null;
	datalen = 0;
	filename = null;
	mimetype = null;
	nuked = true;
	creator_cache = null;
	text_cache = null;

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

      } // end finally

      // record what we did in an audit record
      ar = env.newAudit(AuditRecord.NUKE_MESSAGE,"conf=" + env.getConfID() + ",post=" + postid);

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

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

    } // end finally

  } // end nuke

  public void attachData(String m_type, String file, int length, InputStream data)
      throws AccessError, DataException
  {
    if (logger.isDebugEnabled())
      logger.debug("attachData to msg " + postid + ": (" + m_type + ", " + file + ", " + length + ")");

    if (mimetype!=null)
    { // the message already has an attachment
      logger.error("tried to attach data to a message that already has it!");
      throw new AccessError("This message already has an attachment.");

    } // end if

    if (creator_uid!=env.getUserID())
    { // you can't attach to this message!
      logger.error("tried to attach data to a message that's not yours!");
      throw new AccessError("You are not permitted to add an attachment to this message.");

    } // end if

    if (nuked || (scribble_date!=null))
    { // this would be an exercise in futility!
      logger.error("cannot attach to a nuked or scribbled message");
      throw new AccessError("You cannot attach data to a message that no longer exists.");

    } // end if

    if (StringUtil.isStringEmpty(m_type))
    { // no MIME type specified
      logger.error("no MIME type specified for attachment");
      throw new AccessError("MIME type of attachment data not specified.");

    } // end if

    if (StringUtil.isStringEmpty(file))
    { // no MIME type specified
      logger.error("no filename specified for attachment");
      throw new AccessError("Filename of attachment data not specified.");

    } // end if

    if (length<=0)
    { // a length of 0 or less is just WRONG
      logger.error("non-positive length specified for attachment");
      throw new AccessError("Invalid attachment length.");

    } // end if
    else if (length>MAX_ATTACH)
    { // the attachment is too damn long!
      logger.error("attachment is too long (" + length + " bytes)");
      throw new AccessError("The attachment is too long to store.  Maximum available length is " + MAX_ATTACH
			    + " bytes.");

    } // end else if

    // Compress the attachment data in memory.
    InputStream real_data = null;
    int real_length = 0;
    int tmp_stgmethod;

    if (env.getEngine().isNoCompressMimeType(m_type))
    { // don't compress me, just store me normally
      logger.debug("stored as NORMAL data (no compression)");
      real_data = data;
      real_length = length;
      tmp_stgmethod = 0;  // indicates uncompressed data

    } // end if
    else
    { // OK, we want to compress the data
      logger.debug("stored as COMPRESSED data");

      try
      { // create a compressor into a memory buffer
	ByteArrayOutputStream bytestm = new ByteArrayOutputStream(length);
	GZIPOutputStream gzipstm = new GZIPOutputStream(bytestm);

	// do the usual read/write loop to get the data compressed
	byte[] buffer = new byte[4096];
	int rd = data.read(buffer);
	while (rd>=0)
	{ // write, then read
	  if (rd>0)
	    gzipstm.write(buffer,0,rd);
	  rd = data.read(buffer);

	} // end while

	// finish compression, then get the data and the real length
	gzipstm.finish();
	buffer = bytestm.toByteArray();
	real_length = buffer.length;
	real_data = new ByteArrayInputStream(buffer);
	gzipstm.close();
	if (logger.isDebugEnabled())
	  logger.debug("org size " + length + ", compr size = " + real_length + " ("
		       + String.valueOf(100 - ((real_length * 100) / length)) + "% reduction)");

      } // end try
      catch (IOException ioe)
      { // the big error
	logger.error("IOException in compressor loop: " + ioe.getMessage(),ioe);
	throw new DataException("Failure in compressing read data: " + ioe.getMessage());

      } // end catch

      tmp_stgmethod = 1;  // indicates compressed data

    } // end else
    
    Connection conn = null;
    AuditRecord ar = null;
    PreparedStatement stmt = null;

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

      // make sure we have the right status to upload
      refresh(conn);
      if (nuked || (scribble_date!=null))
      { // this would be an exercise in futility!
	logger.error("cannot attach to a nuked or scribbled message");
	throw new AccessError("You cannot attach data to a message that no longer exists.");

      } // end if

      // Build the SQL statement that inserts the attachment.  Note the use of the "?" to specify the
      // BLOB parameter (the attachment data itself); this comes later.
      StringBuffer sql = new StringBuffer("INSERT INTO postattach (postid, datalen, last_hit, stgmethod, "
					  + "filename, mimetype, data) VALUES (");
      sql.append(postid).append(", ").append(length).append(", '");
      sql.append(SQLUtil.encodeDate(new java.util.Date())).append("', ").append(tmp_stgmethod).append(", '");
      sql.append(SQLUtil.encodeString(file)).append("', '").append(SQLUtil.encodeString(m_type));
      sql.append("', ?);");
      if (logger.isDebugEnabled())
	logger.debug("SQL: " + sql.toString());

      // Prepare the statement, set the BLOB parameter, and execute it.
      stmt = conn.prepareStatement(sql.toString());
      stmt.setBinaryStream(1,real_data,real_length);
      stmt.executeUpdate();

      // Save off the local attachment values.
      logger.debug("... attachment completed successfully");
      datalen = length;
      filename = file;
      mimetype = m_type;
      stgmethod = tmp_stgmethod;

      // Generate an audit record indicating what we did.
      ar = env.newAudit(AuditRecord.UPLOAD_ATTACHMENT,"conf=" + env.getConfID() + ",post=" + postid,
			"len=" + length + ",type=" + m_type + ",name=" + file + ",method=" + tmp_stgmethod);

    } // end try
    catch (SQLException e)
    { // turn this into a DataException
      logger.error("DB error saving attachment: " + e.getMessage(),e);
      throw new DataException("unable to save attachment data: " + e.getMessage(),e);

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

    } // end finally

  } // end attachData

  public void attachData(String m_type, String file, byte[] data) throws AccessError, DataException
  {
    attachData(m_type,file,data.length,new ByteArrayInputStream(data));

  } // end attachData

  public boolean canPublish()
  {
    if (!(env.testPermission(EnvUser.PERM_PUBLISH_FP)))
    { // must be a sysadmin to publish
      if (logger.isDebugEnabled())
	logger.debug("canPublish(): permission test failed");
      return false;

    } // end if

    if ((scribble_date!=null) || nuked)
      return false;  // cannot publish a scribbled or nuked message

    Connection conn = null;
    Statement stmt = null;

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

      // see if the post has already been published
      ResultSet rs = stmt.executeQuery("SELECT by_uid FROM postpublish WHERE postid = " + postid
				       + " LIMIT 1;");
      return !(rs.next());

    } // end try
    catch (SQLException e)
    { // just trap SQL exceptions and log them
      logger.error("SQL exception in TopicMessageUserContextImpl.canPublish: " + 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;  // assume we can't

  } // end canPublish

  public void publish() throws DataException, AccessError
  {
    env.testPermission(EnvUser.PERM_PUBLISH_FP,"You are not permitted to publish postings to the front page.");
    if (nuked)
    { // we can't publish a nuked message!
      logger.error("unable to publish because message nuked");
      throw new DataException("Cannot publish a message that has been nuked.");

    } // end if

    if (scribble_date!=null)
    { // we can't publish a scribbled message!
      logger.error("unable to publish because message scribbled");
      throw new DataException("Cannot publish a message that has been scribbled.");

    } // end if

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

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

      // see if post has already been published
      ResultSet rs = stmt.executeQuery("SELECT by_uid FROM postpublish WHERE postid = " + postid
				       + " LIMIT 1;");
      if (rs.next())  // can't do it, friend!
	throw new DataException("This posting has already been published.");

      boolean done = false;
      env.getEngine().startPublish();

      try
      { // insert the post reference into the database
	StringBuffer sql =
	    new StringBuffer("INSERT INTO postpublish (sigid, postid, by_uid, on_date) VALUES (");
	sql.append(env.getCommunityID()).append(", ").append(postid).append(", ").append(env.getUserID());
	java.util.Date now = new java.util.Date();
	sql.append(", '").append(SQLUtil.encodeDate(now)).append("');");
	stmt.executeUpdate(sql.toString());

	// generate an audit record indicating what we've done
	ar = env.newAudit(AuditRecord.PUBLISH_POST,"conf=" + env.getConfID() + ",post=" + postid);

	// establish cached data object for front page
	env.getEngine().publishNew(new PublishedMessageImpl(env,env.getCommunityID(),postid,parent,topicid,
							    num,linecount,creator_uid,posted,pseud,
							    creator_cache,text_cache));
	done = true;

      } // end try
      finally
      { // make sure to release the lock if we goofed in here
	if (!done)
	  env.getEngine().publishNew(null);

      } // end finally

    } // end try
    catch (SQLException e)
    { // just trap SQL exceptions and log them
      logger.error("unable to publish posting: " + e.getMessage(),e);
      throw new DataException("unable to publish posting: " + e.getMessage(),e);

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

    } // end finally

  } // end publish

  public TopicContext getEnclosingTopic() throws DataException
  {
    return TopicUserContextImpl.getTopicByID(env,topicid);

  } // end getEnclosingTopic

  public String getPostLink() throws DataException
  {
    if (nuked)
      return null;
    String prefix = env.getConference().selfConference().getPostLink();
    short topicnum = -1;
    Connection conn = null;
    Statement stmt = null;

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

      ResultSet rs = stmt.executeQuery("SELECT num FROM topics WHERE topicid = " + topicid + ";");
      if (!(rs.next()))
	throw new DataException("unable to locate topic number for topic ID " + topicid);
      topicnum = rs.getShort(1);

    } // end try
    catch (SQLException e)
    { // just trap SQL exceptions and log them
      logger.error("unable to get topic number: " + e.getMessage(),e);
      throw new DataException("unable to get topic number: " + e.getMessage(),e);

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

    } // end finally

    // composite together the post link value
    return prefix + "." + topicnum + "." + num;

  } // end getPostLink

  public void moveToTopic(int newtopicid) throws DataException, AccessError
  {
    if (!(env.getConference().userCanNuke()) || !(env.getConference().userCanPost()))
    { // we must have both Nuke and Post privileges in this conference to be able to move
      logger.error("trying to move post w/o permission!");
      throw new AccessError("You are not permitted to move this message to another topic.");

    } // end if

    if (newtopicid==topicid)
      return;  // attempting to move to the same topic is a no-op

    if (nuked)
    { // we can't move a nuked message!
      logger.error("unable to move message because message nuked");
      throw new DataException("Cannot move a message that has been nuked.");

    } // end if

    if (scribble_date!=null)
    { // we can't move a scribbled message!
      logger.error("unable to move because message scribbled");
      throw new DataException("Cannot move a message that has been scribbled.");

    } // end if

    Connection conn = null;
    Statement stmt = null;
    AuditRecord ar = null;
    ArrayList mailto_addrs = null;
    int new_topic_num = -1;
    String new_topic_name = null, my_text = null;
    ResultSet rs;
    StringBuffer sql = new StringBuffer();

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

      // lock the tables we reference
      stmt.executeUpdate("LOCK TABLES confs WRITE, topics WRITE, posts WRITE, topicsettings READ;");

      ArrayList mailto_uids = null;
      int old_topicid;

      try
      { // verify that we're not trying to move post 0 when that's the only one that exists
        if (num==0)
	{ // if we're moving post 0, check the number of posts in the topic
	  rs = stmt.executeQuery("SELECT top_message FROM topics WHERE topicid = " + topicid + ";");
	  if (!(rs.next()))
	    throw new InternalStateError("unable to get current message's topic?!?!?");
	  if (rs.getInt(1)==0)
	    throw new DataException("You cannot move the only message out of a topic.");

	} // end if

	// verify that the target topic exists
	rs = stmt.executeQuery("SELECT confid, num, top_message, name FROM topics WHERE topicid = "
			       + newtopicid + ";");
	if (!(rs.next()))
	  throw new DataException("The specified topic to move the message to does not exist.");
	if (env.getConfID()!=rs.getInt(1))
	  throw new AccessError("The topic to move the message to must be within the same conference.");
	int newconfid = rs.getInt(1);
	new_topic_num = rs.getInt(2);
	int new_post_num = rs.getInt(3) + 1;
	new_topic_name = rs.getString(4);

	// save off the old parent, topic ID, and old post number
	long old_parent = parent;
	old_topicid = topicid;
	int old_post_num = num;

	// adjust the post record in the database so it's part of the new topic
	sql.append("UPDATE posts SET parent = 0, topicid = ").append(newtopicid).append(", num = ");
	sql.append(new_post_num).append(" WHERE postid = ").append(postid).append(';');
	if (logger.isDebugEnabled())
	  logger.debug("SQL: " + sql.toString());
	stmt.executeUpdate(sql.toString());
	parent = 0;
	topicid = newtopicid;
	num = new_post_num;

	// Touch the topic values to reflect the added post.
	sql.setLength(0);
	java.util.Date now = new java.util.Date();
	sql.append("UPDATE topics SET top_message = ").append(new_post_num).append(", lastupdate = '");
	sql.append(SQLUtil.encodeDate(now)).append("' WHERE topicid = ").append(newtopicid).append(';');
	if (logger.isDebugEnabled())
	  logger.debug("SQL: " + sql.toString());
	stmt.executeUpdate(sql.toString());

	// Now we have to renumber the remaining posts in the "old" topic, just as if the post had been
	// nuked rather than moved.
	// -----
	// Phase 1 of renumber - Renumber all posts IN THIS TOPIC that had a number higher than the one
	// we just shifted over.
	stmt.executeUpdate("UPDATE posts SET num = (num - 1) WHERE topicid = " + old_topicid + " AND num > "
			   + old_post_num + ";");

	// Phase 2 of renumber - The topic now has one less post in it.  Reflect that.
	stmt.executeUpdate("UPDATE topics SET top_message = (top_message - 1) WHERE topicid = "
			   + old_topicid + ";");

	// Also reparent any posts that had this one as a parent.
	stmt.executeUpdate("UPDATE posts SET parent = " + old_parent + " WHERE parent = " + postid + ";");

	// Touch the "conference updated" indicator.
	env.getConference().touchUpdate(conn,now);

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

	} // end while

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

      } // end finally

      // record what we did in an audit record
      ar = env.newAudit(AuditRecord.MOVE_MESSAGE,"conf=" + env.getConfID() + ",post=" + postid,
			"fromtopic=" + old_topicid,"totopic=" + newtopicid);

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

	// also load the message's text for E-mailing
	my_text = this.getBodyText();

      } // end if

    } // end try
    catch (SQLException e)
    { // just trap SQL exceptions and log them
      logger.error("unable to move message: " + e.getMessage(),e);
      throw new DataException("unable to move message: " + e.getMessage(),e);

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

    } // end finally

    if (mailto_addrs!=null)
    { // spin off a background thread to generate the E-mails
      env.getGlobalSite().queueTask(new PostDeliveryAgent(env,my_text,pseud,new_topic_name,new_topic_num,
							  mailto_addrs));

    } // end if

  } // end moveToTopic

  public boolean canMove()
  {
    return (env.getConference().userCanNuke() && env.getConference().userCanPost());

  } // end canMove

  /*--------------------------------------------------------------------------------
   * External static operations
   *--------------------------------------------------------------------------------
   */

  static final List loadMessageRange(EnvConference env, int topicid, int post_low, int post_high)
      throws DataException
  {
    if (logger.isDebugEnabled())
      logger.debug("loadMessageRange for conf # " + env.getConfID() + ", topic #" + topicid + ", range ["
		   + post_low + ", " + post_high + "]");

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

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

      // run a query to get all the posts in a particular topic
      StringBuffer sql =
	  new StringBuffer("SELECT p.postid, p.parent, p.num, p.linecount, p.creator_uid, p.posted, "
			   + "p.hidden, p.scribble_uid,	p.scribble_date, p.pseud, a.datalen, a.filename, "
			   + "a.mimetype, a.stgmethod FROM posts p LEFT JOIN postattach a ON "
			   + "p.postid = a.postid WHERE p.topicid = ");
      sql.append(topicid).append(" AND p.num >= ").append(post_low).append(" AND p.num <= ");
      sql.append(post_high).append(" ORDER BY p.num ASC;");
      if (logger.isDebugEnabled())
	logger.debug("SQL: " + sql.toString());
      ResultSet rs = stmt.executeQuery(sql.toString());

      while (rs.next())
      { // create implementation objects and shove them into the return vector
	TopicMessageContext val =
	  new TopicMessageUserContextImpl(env,rs.getLong(1),rs.getLong(2),topicid,rs.getInt(3),rs.getInt(4),
					  rs.getInt(5),SQLUtil.getFullDateTime(rs,6),rs.getBoolean(7),
					  rs.getInt(8),SQLUtil.getFullDateTime(rs,9),rs.getString(10),
					  rs.getInt(11),rs.getString(12),rs.getString(13),rs.getInt(14));
	rc.add(val);

      } // end while

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

  } // end loadMessageRange

  static final TopicMessageContext loadMessage(EnvConference env, int topicid, int message_num)
      throws DataException
  {
    if (logger.isDebugEnabled())
      logger.debug("loadMessage for conf # " + env.getConfID() + ", topic #" + topicid + ", message "
		   + message_num);

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

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

      // run a query to get all the posts in a particular topic
      StringBuffer sql =
	  new StringBuffer("SELECT p.postid, p.parent, p.num, p.linecount, p.creator_uid, p.posted, "
			   + "p.hidden, p.scribble_uid,	p.scribble_date, p.pseud, a.datalen, a.filename, "
			   + "a.mimetype, a.stgmethod FROM posts p LEFT JOIN postattach a ON "
			   + "p.postid = a.postid WHERE p.topicid = ");
      sql.append(topicid).append(" AND p.num = ").append(message_num).append(';');
      if (logger.isDebugEnabled())
	logger.debug("SQL: " + sql.toString());
      ResultSet rs = stmt.executeQuery(sql.toString());

      if (rs.next())  // create an object reference and return it
	return new TopicMessageUserContextImpl(env,rs.getLong(1),rs.getLong(2),topicid,rs.getInt(3),
					       rs.getInt(4),rs.getInt(5),SQLUtil.getFullDateTime(rs,6),
					       rs.getBoolean(7),rs.getInt(8),SQLUtil.getFullDateTime(rs,9),
					       rs.getString(10),rs.getInt(11),rs.getString(12),
					       rs.getString(13),rs.getInt(14));

      // indicates an error...
      throw new DataException("Message not found.");

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

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

    } // end finally

  } // end loadMessage

  static final TopicMessageContext getMessage(EnvConference env, long postid) throws DataException
  {
    if (logger.isDebugEnabled())
      logger.debug("getMessage for conf # " + env.getConfID() + ", post #" + postid);

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

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

      StringBuffer sql =
	  new StringBuffer("SELECT p.postid, p.parent, p.topicid, p.num, p.linecount, p.creator_uid, "
			   + "p.posted, p.hidden, p.scribble_uid, p.scribble_date, p.pseud, a.datalen, "
			   + "a.filename, a.mimetype, a.stgmethod FROM topics t, posts p LEFT JOIN "
			   + "postattach a ON p.postid = a.postid WHERE t.topicid = p.topicid AND "
			   + "t.confid = ");
      sql.append(env.getConfID()).append(" AND p.postid = ").append(postid).append(';');
      if (logger.isDebugEnabled())
	logger.debug("SQL: " + sql.toString());
      ResultSet rs = stmt.executeQuery(sql.toString());

      if (rs.next())  // create an object reference and return it
	return new TopicMessageUserContextImpl(env,rs.getLong(1),rs.getLong(2),rs.getInt(3),rs.getInt(4),
					       rs.getInt(5),rs.getInt(6),SQLUtil.getFullDateTime(rs,7),
					       rs.getBoolean(8),rs.getInt(9),SQLUtil.getFullDateTime(rs,10),
					       rs.getString(11),rs.getInt(12),rs.getString(13),
					       rs.getString(14),rs.getInt(15));

      // indicates an error...
      throw new DataException("Message not found.");

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

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

    } // end finally

  } // end getMessage

} // end class TopicMessageUserContextImpl
