/* oqtane (OpenQueue server classes and daemon)
*
* Copyright (c) 2000 oqtane Development Team (See Credits file)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.openqueue.oqtane;
import java.awt.Toolkit;
import java.net.*;
import java.io.*;
import java.sql.*;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Stack;
import java.util.StringTokenizer;
import java.util.Vector;
public class OQTopic {
public static Vector OQTopics;
public final static int iDefaultTopicDBaseRowsAtStartup = 20;
public final static int iDefaultTopicMaxItemsInRAM = 500;
protected int topicID;
protected String topicName;
protected int owner;
protected String aboutURL;
protected boolean useAboutURLforItems;
protected int flags;
protected int readFlags = 1; // 0=nobody,1=owner,2=admin,4=ACL,8=world
protected Vector topicItems;
protected boolean persistent = true;
protected static Hashtable pathsHash = new Hashtable ();
private OQServer oqServer;
private int latestMessageID;
public int getLatestMessageID() {
return latestMessageID;
}
public synchronized int getFirstIDinItems() {
/* For example, if we erased the first 10,000 messages,
* and first one left in vector is 10001.
*/
if (topicItems.size() > 0) {
OQItem anItem = (OQItem) topicItems.elementAt (0);
return anItem.getMessageID();
} else {
return 0;
}
}
static {
if (OQTopics == null) OQTopics = new Vector ();
}
static void loadTopics (OQServer aServer) {
String aCmd = new String();
String aTopicName = new String();
String aOwner = new String();
String anID = new String();
String aFlag = new String();
OQServer.report(OQServer.repDebug, "Loading topics via database.");
OQDBAccess aDB = aServer.GetDB();
synchronized(aDB) {
try {
String sql = OQDBAccess.getSQLCommand(OQDBAccess.sql_SELECTALLTOPICS);
ResultSet rs = aDB.getStatement().executeQuery( sql );
OQDBAccess dbForItems = new OQDBAccess();
OQServer.report (OQServer.repNormal, "Done new dbForItems");
if ((dbForItems != null) && ( dbForItems.getConnectedOK()) ) {
OQServer.report (OQServer.repDebugMinor, "Starting while loop");
while(rs.next()) {
// For some strange reason, the JDBC-ODBC Bridge with
// MSAccess only allow one read of a field value
//
long rsTopicID = rs.getLong(1) ;
String rsTitle = rs.getString(2) ;
long rsOwner = rs.getLong(3) ;
long rsLastMessageID = rs.getLong(4);
int rsPersistent = rs.getInt(5);
int rsReadFlags = rs.getInt(6);
OQServer.report(OQServer.repDebug,
"TopicID=" +rsTopicID + " " +
"Title=" +rsTitle + " " +
"Owner=" +rsOwner + " " +
"LastMsgID=" +rsLastMessageID + " " +
"Persistent=" +rsPersistent + " " +
"ReadFlags =" +rsReadFlags );
int pers = rsPersistent;
int tmpReadFlags = rsReadFlags;
OQServer.report(OQServer.repDebug, "---");
OQTopic bTopic = new OQTopic ( aServer,
(int) rsTopicID, rsTitle, (int) rsOwner, (int) rsLastMessageID, (int) rsPersistent, (int) rsReadFlags);
if (pers == 1) { /* should make it a bitwise test? */
bTopic.persistent = true;
OQServer.report(OQServer.repDebug, "-PERSISTENT-");
} else {
bTopic.persistent = false;
OQServer.report(OQServer.repDebug, "-Not Persistent-");
}
OQServer.report (OQServer.repDebug, "(In this version, not pre-loading dbForItems.)");
// OQServer.report (OQServer.repDebug, "Loading dbForItems");
// bTopic.loadItems (dbForItems);
// OQServer.report (OQServer.repDebug, "End of While Loop");
}
} else {
OQServer.report(OQServer.repError, "ERROR in OQTopic while creating connection to load topic items.");
}
dbForItems.CloseAccess();
rs.close();
/* Now load in paths. It's permissible to have more than 1 path
* for one topic, so we need to do this in a separate loop.
*/
sql = OQDBAccess.getSQLCommand(OQDBAccess.sql_SELECTTOPICPATHS);
rs = aDB.getStatement().executeQuery( sql );
OQServer.report (OQServer.repDebug, "Starting to process topic paths... ");
while(rs.next()) {
// For some strange reason, the JDBC-ODBC Bridge with
// MSAccess only allow one read of a field value
//
long rsTopicID2 = rs.getLong(1) ;
String rsPath = rs.getString(2) ;
OQServer.report(OQServer.repDebug,
"TopicID=" +rsTopicID2 + " " +
"Path=" +rsPath );
OQTopic bTopic = OQTopic.getByID((int) rsTopicID2);
if (bTopic != null) {
OQTopic.pathsHash.put(rsPath, bTopic);
}
}
OQServer.report (OQServer.repDebug, "Done processing topic paths. ");
rs.close();
} catch( Exception e ) {
OQServer.report(OQServer.repError, e.getMessage());
}
} /* synchronized */
}
static int totalTopics () {
return OQTopics.size ();
}
public int nextVersion() {
latestMessageID = latestMessageID + 1;
return latestMessageID;
}
public OQTopic (OQServer aServer, int anID, String aTopicName, int aOwner, int aLastMsgID, int aFlag, int aReadFlags) {
oqServer = aServer;
topicID = anID;
topicName = aTopicName;
owner = aOwner;
aboutURL = null;
useAboutURLforItems = false;
latestMessageID = aLastMsgID;
flags = aFlag;
if (aFlag == 1) {
persistent = true;
} else {
persistent = false;
}
readFlags = aReadFlags;
topicItems = new Vector (OQTopic.iDefaultTopicDBaseRowsAtStartup);
OQTopics.addElement (this);
OQServer.report(OQServer.repDebug, Long.toString(topicID) + " = " + topicName + " current Topics = " + OQTopics.size ());
}
/* topicPathExists() checks the database to see if that
* path is taken.
* Return 0 if it doesn't exist, else return the topicid.
*/
public static int topicPathExists (OQServer anOQServer, String aTopicPath) {
int iTopicID = 0;
OQDBAccess aDB = anOQServer.GetDB();
synchronized(aDB) {
try {
String sql = OQDBAccess.getSQLCommand(OQDBAccess.sql_SELECTTOPICBYPATH, aTopicPath);
ResultSet rs = aDB.getStatement().executeQuery( sql );
if (rs.next() == true) {
iTopicID = (int) rs.getLong(1);
}
rs.close();
} catch (Exception e) {
OQServer.report(OQServer.repError, "Exception while trying to lookup topic path in database.");
iTopicID = 0;
}
}
return iTopicID;
}
// create topic in SQL database, then in memory
// This is only to be called after initial server setup,
// such as when administrator wants to create a new topic.
public static boolean createNewTopic(OQServer anOQServer, String
sTempTopicPath, String sTempTopicTitle, int iTempUser, int iPersistent, int iReadFlags) {
boolean succeeded = false;
OQDBAccess aDB = anOQServer.GetDB();
synchronized(aDB) {
try {
String addTopicQuery1 = OQDBAccess.getSQLCommand(OQDBAccess.sql_CREATETOPIC,
sTempTopicTitle,
String.valueOf( iTempUser ),
String.valueOf( iPersistent ),
String.valueOf( iReadFlags ) );
String addTopicQuery2 = OQDBAccess.getSQLCommand(OQDBAccess.sql_SELECTMAXTOPICID);
int updateResult = aDB.getStatement().executeUpdate( addTopicQuery1 );
ResultSet rs = aDB.getStatement().executeQuery( addTopicQuery2 );
if (rs.next()==true) {
int rsTopicID = (int) rs.getLong(1);
String addTopicQuery3 = OQDBAccess.getSQLCommand(OQDBAccess.sql_INSERTPATH,
sTempTopicPath, String.valueOf( rsTopicID ) );
int updateResultPath = aDB.getStatement().executeUpdate( addTopicQuery3 );
OQTopic tTempTopic = new OQTopic (anOQServer, rsTopicID, sTempTopicTitle, iTempUser, 0, iPersistent, iReadFlags) ;
OQTopic.pathsHash.put(sTempTopicPath, tTempTopic);
succeeded = true;
}
} catch (Exception e) {
OQServer.report(OQServer.repError, "Exception while trying to add topic to database.");
System.out.println(e.getMessage());
}
}
return succeeded;
}
public synchronized void InsertSystemMessage(String Subject, String body) {
OQServer.report(OQServer.repDebugMinor, "Inserting system message. " + Subject);
OQItem theItem;
String theHeaders = new String ("Subject: " + Subject);
Date rightNow = new Date();
theHeaders = theHeaders + "\r\n" + "Posted: " + rightNow.toGMTString();
theItem = addItem(Subject, theHeaders, body);
}
public boolean userHasReadPermission(OQUser aUser) {
if ((readFlags & 8) == 8) { // world
return true;
} else {
if ( ((readFlags & 1) == 1) && (owner==aUser.UserID) ) { // owner
return true;
} else {
if ( ((readFlags & 2) == 2) && (aUser.isAdmin()==true) ) { // admin
return true;
} else {
return false;
}
}
}
}
public void loadItems (OQDBAccess newDB) {
OQServer.report(OQServer.repNormal, "Pre-loading messages via database for topic " + topicID + ".");
try {
OQServer.report(OQServer.repDebugMinor, "Just entered try... " + topicID + ".");
Statement NewStmt = newDB.getConnection().createStatement();
String sql = OQDBAccess.getSQLCommand(OQDBAccess.sql_LOADMESSAGES, String.valueOf(topicID), String.valueOf(OQTopic.iDefaultTopicDBaseRowsAtStartup) );
OQServer.report(OQServer.repDebugMinor, sql );
ResultSet rsItems = NewStmt.executeQuery( sql );
ResultSetMetaData rsmd = rsItems.getMetaData();
int iColumnCount = rsmd.getColumnCount();
if (rsmd.getColumnCount() != 4) {
OQServer.report(OQServer.repError, "Wrong number of columns, some JDBC weirdness. Skipping this topic.");
} else {
Stack stk = new Stack();
OQItem bItem = null;
int iLoop_RowsDone = 0;
while ((iLoop_RowsDone < OQTopic.iDefaultTopicDBaseRowsAtStartup) && (rsItems.next()) ) {
OQServer.report(OQServer.repDebug, " msg... ");
long aMessageID = 0;
String aMessageBody = "";
String aSubject = "";
String aHeaders = "";
boolean isMessageLoadedOK = true;
try {
aMessageID = rsItems.getLong(1);
aMessageBody = rsItems.getString(2);
aSubject = rsItems.getString(3);
aHeaders = rsItems.getString(4);
} catch (Exception ex) {
/* for example, a null value for messagedata? */
OQServer.report(OQServer.repNormal, "Error loading message for topic " + topicID + ": " + ex.getMessage());
isMessageLoadedOK = false;
}
if (isMessageLoadedOK == true) {
bItem = new OQItem ((int) aMessageID, aSubject );
if (aHeaders != null) {
bItem.setHeaders ( aHeaders );
}
if (aMessageBody != null) {
bItem.setMessage ( aMessageBody );
}
stk.push(bItem);
}
iLoop_RowsDone++;
}
while (stk.empty() == false) {
bItem = (OQItem) stk.pop();
topicItems.addElement (bItem);
}
}
OQServer.report(OQServer.repDebug, " - all items done for this topic -" );
rsItems.close();
NewStmt.close();
OQServer.report(OQServer.repDebug, " - rsItems and NewStmt now closed for this topic. -" );
} catch( Exception e ) {
OQServer.report(OQServer.repError, e.getMessage());
}
}
/* broadcast puts an item into the queues for all OQUpdaters
* (all users currently active).
*/
/* May 20, 2000 - MattJ: Woah, deadlock issue here.
* We're locking the updater here before locking the topic.
* To be consistent with OQUser, we must lock topic, then updater.
* So, I'm adding a lock on the topic first.
*/
public synchronized void broadcast (OQItem theItem){
OQUserCmdHandler aUserCmdHandler;
Enumeration enum = OQUserCmdHandler.userCmdHandlers.elements ();
while (enum.hasMoreElements ()) {
try {
aUserCmdHandler = (OQUserCmdHandler) enum.nextElement ();
/* User might not be known yet if we just connected,
* because we might be inserting a system message to
* indicate that we just connected. So test for null.
*/
/* !!!!! this next line seems to cause possible
concurrency issue when it makes the getState call.
Look into it! (May 18, 2000) */
if ((aUserCmdHandler.getState() != OQUserCmdHandler.state_UNAUTHORIZED) && (aUserCmdHandler.thisUser != null)) {
if ( aUserCmdHandler.thisUser.getSubscriptionByID(topicID) != null ) {
/* A user who is subscribed to this topic is "live" now,
* with a UserCmdHandler handling the connection, so let's
* stick this item in it's queue.
*/
if (aUserCmdHandler.theUpdater != null) {
// added next two lines to use proper lock order. MJ:2000/05/20
OQTopic aTopic = OQTopic.getByID(topicID);
synchronized (aTopic) {
synchronized (aUserCmdHandler.theUpdater) {
aUserCmdHandler.theUpdater.addItem(topicID, theItem.getMessageID() );
aUserCmdHandler.theUpdater.notify();
}
}
} /* updater not null */
}
}
} catch (Exception ex) {
OQServer.report(OQServer.repError, "Error in broadcast loop. Bad next element? " + ex.getMessage() );
}
} /* while more connections to check */
}
public OQItem addItem(String aSubject, String theHeaders, String messageData) {
/* First, check that content-length: exists */
String upHeaders = theHeaders.toUpperCase();
if (upHeaders.indexOf("\r\nCONTENT-LENGTH: ") == -1) {
/* This header is missing, so let's add it. */
theHeaders = theHeaders + "\r\nContent-Length: " + Integer.toString(messageData.length()) ;
}
OQItem bItem;
OQItem resultItem = null;
try {
bItem = new OQItem (nextVersion(), aSubject);
bItem.setHeaders (theHeaders ) ;
bItem.setMessage (messageData);
if (updateVersionInTopicsFile() == true) {
if (storeMessagePermanently (bItem) == true) {
synchronized (topicItems) {
topicItems.addElement (bItem);
while (topicItems.size() > iDefaultTopicMaxItemsInRAM ) {
topicItems.removeElementAt(0);
}
}
broadcast (bItem);
// success, so return our new item.
resultItem = bItem;
}
}
} catch (Exception ex) {
}
return resultItem;
}
/* Record permanently that user has seen the message. */
public synchronized boolean updateVersionInTopicsFile () {
boolean success = false;
if (persistent) {
OQDBAccess aDB = oqServer.GetDB();
synchronized (aDB) {
String sql = OQDBAccess.getSQLCommand(OQDBAccess.sql_UPDATELASTMESSAGEID, String.valueOf(latestMessageID), String.valueOf(topicID) );
success = aDB.doUpdate( sql );
}
} else {
success = true;
}
return success;
}
// return true on success, false on failure
public synchronized boolean storeMessagePermanently (OQItem anItem) {
boolean success = true;
if (persistent) {
OQDBAccess aDB = oqServer.GetDB();
synchronized(aDB) {
try {
String sql = OQDBAccess.getSQLCommand(OQDBAccess.sql_STOREMESSAGE,
String.valueOf( topicID ),
String.valueOf( anItem.getMessageID() ),
anItem.getSubject(),
String.valueOf( anItem.getMessageLength() ),
anItem.getMessage(),
anItem.getHeaders() );
success = aDB.doUpdate( sql );
} catch (Exception ex) {
success = false;
OQServer.report(OQServer.repError, "Error in storeMessagePermanently: " + ex);
}
}
}
return success;
}
/* getByName() looks up an existing OQTopic, or returns null
* if none match. */
public static OQTopic getByName (String aName) {
OQTopic aTopic;
OQTopic result;
result = null;
synchronized (OQTopics) {
Enumeration enum = OQTopics.elements ();
while (enum.hasMoreElements ()) {
aTopic = (OQTopic) enum.nextElement ();
if ( aTopic.topicName.equals (aName)) result = aTopic;
}
}
return result;
}
/* getByReference() receives a string which is either
* a path ("/foo/bar") or a topic number ("#101").
* This function determines which it is, and calls either
* getByID() or getByName().
*/
public static OQTopic getByReference (String sRef) {
if (sRef.startsWith("#")) {
// it's a number (unless there's an error)
int aTopicID = -1;
try {
aTopicID = Integer.parseInt(sRef.substring(1,sRef.length()));
return OQTopic.getByID(aTopicID);
} catch (Exception ex) {
return null;
}
} else {
// it's a path
return OQTopic.getByPath(sRef);
}
}
public static OQTopic getByPath (String sPath) {
OQTopic result;
result = (OQTopic) OQTopic.pathsHash.get(sPath);
return result;
}
/* getByID() looks up an existing OQTopic, or returns null if none match. */
public static OQTopic getByID (int anID) {
OQTopic aTopic;
OQTopic result;
result = null;
synchronized (OQTopics) {
Enumeration enum = OQTopics.elements ();
while (enum.hasMoreElements ()) {
aTopic = (OQTopic) enum.nextElement ();
if ( aTopic.topicID == anID ) {
result = aTopic;
} else {
}
}
}
return result;
}
/* getItemByID() finds an existing OQItem for a given OQTopic,
* or returns null if no match is found.
*/
public OQItem getItemByID (int aMessageID) {
OQItem tempItem;
tempItem = null;
if ( (aMessageID > 0) && (topicItems.size() > 0) ) {
OQItem anItem;
try {
anItem = (OQItem) topicItems.elementAt(topicItems.size()-1);
int latestInVector = anItem.getMessageID();
if (aMessageID == latestInVector) {
tempItem = (OQItem) topicItems.elementAt(topicItems.size()-1);
} else {
if (aMessageID > (latestInVector - topicItems.size()) ) {
/* to do: add some smarts here like a boolean search
* after a first-guess of where it should be.
*/
Enumeration enumItems = topicItems.elements ();
while (enumItems.hasMoreElements () && (tempItem == null) ) {
anItem = (OQItem) enumItems.nextElement ();
if ( anItem.getMessageID() == aMessageID ) {
tempItem = anItem;
}
}
} else {
/* XXX - is outside current RAM vector.
* We'll go on to do SQL query, below.
*/
}
}
} catch (Exception ex) {
tempItem = null;
}
if (tempItem == null) {
/* Item isn't in memory. Let's see if it's in the data store. */
OQDBAccess aDB = oqServer.GetDB();
synchronized(aDB) {
try {
Statement NewStmt = aDB.getConnection().createStatement();
String sql = OQDBAccess.getSQLCommand(OQDBAccess.sql_GETMESSAGE, String.valueOf(topicID), String.valueOf(aMessageID) );
ResultSet rs = NewStmt.executeQuery( sql );
if (rs.next()) {
long ourMessageID = rs.getLong(1);
String aMessageBody = rs.getString(2);
String aSubject = rs.getString(3);
String aHeaders = rs.getString(4);
tempItem = new OQItem (aMessageID, aSubject );
if (aHeaders != null) {
tempItem.setHeaders ( aHeaders );
}
if (aMessageBody != null) {
tempItem.setMessage ( aMessageBody );
}
/* topicItems.addElement (bItem); */
}
rs.close();
} catch( Exception e ) {
OQServer.report(OQServer.repError, e.getMessage() );
}
} /* synchronized */
}
}
return tempItem;
}
}