KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > mmbase > applications > community > builders > Message


1 /*
2
3 This software is OSI Certified Open Source Software.
4 OSI Certified is a certification mark of the Open Source Initiative.
5
6 The license (Mozilla version 1.0) can be read at the MMBase site.
7 See http://www.MMBase.org/license
8
9 */

10
11 package org.mmbase.applications.community.builders;
12
13 import org.mmbase.applications.community.modules.*;
14 import java.util.*;
15 import java.io.*;
16
17 import org.mmbase.module.core.*;
18 import org.mmbase.module.corebuilders.*;
19 import org.mmbase.util.*;
20 import org.mmbase.util.logging.*;
21
22 /**
23  * This builder implements additional functionality and methods to handle
24  * community objects.
25  * Added functionality involve posting and removing a message,
26  * managing temporary messages, and retrieving message lists for a
27  * specific 'thread'.
28  *
29  * @author Dirk-Jan Hoekstra
30  * @author Pierre van Rooden
31  * @version $Id: Message.java,v 1.33 2006/08/30 18:05:17 michiel Exp $
32  */

33
34 public class Message extends MMObjectBuilder {
35
36     // errors
37
public final static int POST_OK = 0;
38     public final static int POST_ERROR_UNKNOWN = -1;
39     public final static int POST_ERROR_BODY_EXCEEDS_SIZE = -2;
40     public final static int POST_ERROR_NO_USER = -3;
41     public final static int POST_ERROR_NEED_LOGIN = -4;
42     public final static int POST_ERROR_RELATION_CHANNEL = -5;
43     public final static int POST_ERROR_RELATION_USER = -6;
44     public final static int POST_ERROR_NO_BODY_TEXT = -7;
45     public final static int POST_ERROR_NO_SUBJECT = -8;
46
47     // logger
48
private static final Logger log = Logging.getLoggerInstance(Message.class);
49
50     /** Default tag for the <code>listhead</code> message field */
51     public static final String JavaDoc LIST_HEAD_TAG = "<ul>";
52     /** Default tag for the <code>listtail</code> message field */
53     public static final String JavaDoc LIST_TAIL_TAG = "</ul>";
54
55     /** Field : thread */
56     public static final String JavaDoc F_THREAD = "thread";
57     /** Field : body */
58     public static final String JavaDoc F_BODY = "body";
59     /** Field : subject */
60     public static final String JavaDoc F_SUBJECT = "subject";
61     /** Field : sequence */
62     public static final String JavaDoc F_SEQUENCE = "sequence";
63     /** Field : info */
64     public static final String JavaDoc F_INFO = "info";
65     /** Field : timestamp */
66     public static final String JavaDoc F_TIMESTAMP = "timestamp";
67     /** Field : timestamp in seconds */
68     public static final String JavaDoc F_TIMESTAMPSEC = "timestampsec";
69
70     /** Virtual Field : resubject */
71     public static final String JavaDoc F_RE_SUBJECT = "resubject";
72     /** Virtual Field : replycount */
73     public static final String JavaDoc F_REPLY_COUNT = "replycount";
74     /** Virtual Field : hasreplies */
75     public static final String JavaDoc F_HAS_REPLIES = "hasreplies";
76     /** Virtual Field : parent */
77     public static final String JavaDoc F_PARENT = "parent";
78
79     /** Formatting Function : getinfovalue */
80     public static final String JavaDoc F_GET_INFO_VALUE = "getinfovalue";
81
82     /**
83      * Maximum message body size in bytes.
84      */

85     protected int maxBodySize = 2024;
86     // default expiration time for relation breaker
87
private int expireTime = 1 * 60 * 1000;
88
89     private Channel channelBuilder;
90
91     public TemporaryNodeManager tmpNodeManager = null;
92     private String JavaDoc messageUser = null;
93     // this will be used to genererate keys for temporary nodes used by the community.
94
private int tmpNumbers = 0;
95     private final String JavaDoc tmpNumberPrefix = "cmt";
96
97     // relation breaker for maintaining temporary messages
98
private NodeBreaker chatboxMessages = null;
99
100     // indicates whether this builder has been activated for the community application
101
private boolean active = false;
102
103     /**
104      * Constructor
105      */

106     public Message() {
107     }
108
109     public boolean init() {
110         boolean result = super.init();
111
112         String JavaDoc maxBody = getInitParameter("maxbodysize");
113         if ((maxBody != null) && (maxBody.length() > 0)) {
114             try {
115                 maxBodySize = Integer.parseInt(maxBody);
116             } catch (Exception JavaDoc e) {
117                 log.warn("Invalid value for property'maxbodysize' :" + maxBody);
118             }
119         }
120
121         messageUser = getInitParameter("postas");
122         if ((messageUser == null) || (messageUser.length() == 0)) {
123             messageUser = "system";
124         }
125         tmpNodeManager = new TemporaryNodeManager(mmb);
126         // create relation breaker for maintaining temporary relations
127
chatboxMessages = new NodeBreaker(2 * expireTime, tmpNodeManager);
128
129         // Add temporary fields
130
// These are currently ment to enable the taglib posttag
131
// to pass data used for the info field or to link a message
132
// In future version, these virutal fields migth actually be used to set or get
133
// this data directly from the message node
134
checkAddTmpField("channel"); // node number of the channel object for this message
135
checkAddTmpField("user"); // node number of the user object for this message
136
checkAddTmpField("username"); // username of the person posting the message
137

138         activate();
139
140         return result;
141     }
142
143     /**
144      * Activates the message builder for the community application by associating it with other community builders
145      * @return true if activation worked
146      */

147     public boolean activate() {
148         if (!active) {
149             channelBuilder = (Channel) mmb.getMMObject("channel");
150             active = channelBuilder != null;
151         }
152         return active;
153     }
154
155     // used to retrieve the parent - child role from reldef or cache
156
private int getParentChildRole() {
157         return mmb.getRelDef().getNumberByName("parent/child");
158     }
159
160     // used to retrieve the creator-subject role from reldef or cache
161
private int getCreatorRole() {
162         return mmb.getRelDef().getNumberByName("creator/subject");
163     }
164
165     /**
166      * Obtains the description of an error that occurred during a
167      * user-induced action on a message (such as a post).
168      * @param error the error number
169      * @return the error description
170      */

171     public String JavaDoc getMessageError(int error) {
172         switch (error) {
173         case POST_ERROR_BODY_EXCEEDS_SIZE:
174             return "Message body size exceeds " + maxBodySize + " bytes";
175         case POST_ERROR_NO_USER:
176             return "User name or object needed";
177         case POST_ERROR_NEED_LOGIN:
178             return "User needs to be logged on to post";
179         case POST_ERROR_RELATION_CHANNEL:
180             return "Could not create temporary relations between message and channel.";
181         case POST_ERROR_RELATION_USER:
182             return "Could not create temporary relations between message and user.";
183         case POST_ERROR_NO_BODY_TEXT:
184             return "No message body text specified.";
185         case POST_ERROR_NO_SUBJECT:
186             return "No subject specified.";
187         default :
188             return "Could not post message.";
189         }
190     }
191
192     /**
193      * Post a new message in the channel.
194      *
195      * @param subject The subject of the message.
196      * @param body The body of the message.
197      * @param channel The channel in which the message has to get posted.
198      * @param thread The number of the thread (a Message or Channel) in which the Message has to get listed.
199      * @param chatter The usernumber of the user that has written the message.
200      * @param chatterName The name of the person that has written the message when he hasn't a usernumber.
201      * @return The number of the newly created message node or a negative number if for
202      * some reason no message was created.
203      * The number can be used with getPostError() to get the errormessage.
204      */

205     public int post(String JavaDoc subject, String JavaDoc body, int channel, int thread, int chatter, String JavaDoc chatterName) {
206         if (body.length() == 0) {
207             log.error("post(): no body text");
208             return POST_ERROR_NO_BODY_TEXT;
209         }
210         if (subject.length() == 0) {
211             log.error("post(): no subject");
212             return POST_ERROR_NO_SUBJECT;
213         }
214         if (body.length() > maxBodySize) {
215             log.error("post(): body size exceeds " + maxBodySize + " bytes");
216             return POST_ERROR_BODY_EXCEEDS_SIZE;
217         }
218
219         if (chatterName != null) {
220             if (chatterName.length() == 0) {
221                 log.error("post(): CHATTERNAME must be larger than 0 tokens");
222                 chatterName = null;
223             }
224         }
225         if ((chatterName == null) && (chatter == -1)) {
226             return POST_ERROR_NO_USER;
227         }
228
229         MMObjectNode channelNode = getNode(channel);
230         // test write login
231
// if write-login is true, and no 'chatter' node is specified,
232
// the user has not logged on - so the post should fail
233
if ((channelNode.getIntValue(Channel.F_STATE) & Channel.STATE_WRITE_LOGIN) > 0) {
234             if (chatter == -1) {
235                 return POST_ERROR_NEED_LOGIN;
236             }
237         }
238
239         MMObjectNode node = getNewNode(messageUser);
240         node.setValue(F_SUBJECT, subject);
241         node.setValue(F_BODY, body);
242         node.setValue(F_THREAD, thread);
243
244         node.setValue(F_SEQUENCE, channelBuilder.getNewSequence(channelNode));
245         /*
246          * Make the relation with the MessageTread in which this Message get listed.
247          * And make the relation between message and the uses who posted the message.
248          * Because InsRel keeps a cache of the last 25 'most used' relations wich doesn't
249          * work correct after a insert, delete all cached relations of the parent
250          * messagethread so the search will be done on the database.
251          */

252         if (log.isDebugEnabled()) {
253             log.debug("post(): make relation message with thread and chatter");
254         }
255         InsRel insrel = mmb.getInsRel();
256         if (chatterName != null) {
257             setInfoField(node, "name", chatterName);
258 // node.setValue(F_INFO, "name=\"" + chatterName + "\"");
259
}
260         int id = insert(messageUser, node);
261         if (chatter > 0) {
262             MMObjectNode chattertomsg = insrel.getNewNode(messageUser);
263             chattertomsg.setValue("snumber", chatter);
264             chattertomsg.setValue("dnumber", id);
265             chattertomsg.setValue("rnumber", getCreatorRole());
266             insrel.insert(messageUser, chattertomsg);
267         }
268         MMObjectNode msgtothread = insrel.getNewNode(messageUser);
269         msgtothread.setValue("snumber", id);
270         msgtothread.setValue("dnumber", thread);
271         msgtothread.setValue("rnumber", getParentChildRole());
272         insrel.insert(messageUser, msgtothread);
273         insrel.deleteRelationCache(thread);
274         return id;
275     }
276
277     /**
278      * Posts a message as a temporary message node.
279      *
280      * @param body The body of the message.
281      * @param channel The channel in which the message has to get posted.
282      * @param chatter The usernumber of the user that has written the message.
283      * @return {@link #POST_OK} on success, otherwise an error number.
284      * The number can be used with getPostError() to get the errormessage.
285      * @return <code>true</code. if the message was posted, <code>false</code> if the post failed
286      */

287     public int post(String JavaDoc body, int channel, int chatter) {
288         return post(body, channel, chatter, null);
289     }
290
291     /**
292      * Posts a message as a temporary message node.
293      *
294      * @param body The body of the message.
295      * @param channel The channel in which the message has to get posted.
296      * @param chatter The usernumber of the user that has written the message.
297      * @return {@link #POST_OK} on success, otherwise an error number.
298      * The number can be used with getPostError() to get the errormessage.
299      */

300     public int post(String JavaDoc body, int channel, int chatter, String JavaDoc chatterName) {
301         if (body.length() == 0) {
302             return POST_ERROR_NO_BODY_TEXT;
303         }
304         if (body.length() > maxBodySize) {
305             return POST_ERROR_BODY_EXCEEDS_SIZE;
306         }
307
308         MMObjectNode channelNode = getNode(channel);
309         // test write login
310
// if write-login is true, and no 'chatter' node is specified,
311
// the user has not logged on - so the post should fail
312
if ((channelNode.getIntValue(Channel.F_STATE) & Channel.STATE_WRITE_LOGIN) > 0) {
313             if (chatter == -1)
314                 return POST_ERROR_NEED_LOGIN;
315         }
316
317         // Build a temporary message node.
318
String JavaDoc key = tmpNodeManager.createTmpNode("message", messageUser, getNewTemporaryKey());
319         MMObjectNode message = getNewTmpNode(messageUser, key);
320
321         // Set the fields.
322
int sequence = channelBuilder.getNewSequence(channelNode);
323
324         if (chatterName == null) {
325             chatterName = "unknown";
326             if (chatter != -1) {
327                 MMObjectNode chatternode = getNode(chatter);
328                 if (chatternode != null) {
329                     chatterName = chatternode.getStringValue("gui()");
330                 } else {
331                     chatterName = "chatter_" + chatter;
332                 }
333             }
334         }
335
336         // we have a reference to the temporary node,
337
// so we can change it directly
338
message.setValue(F_BODY, body);
339         message.setValue(F_THREAD, channel);
340         message.setValue(F_SEQUENCE, sequence);
341         boolean useTimeStamp = (getField(F_TIMESTAMP) != null);
342         if (useTimeStamp) {
343             message.setValue(F_TIMESTAMP, System.currentTimeMillis());
344         } else {
345             TimeStamp timeStamp = new TimeStamp();
346             message.setValue("timestampl", timeStamp.lowIntegerValue());
347             message.setValue("timestamph", timeStamp.highIntegerValue());
348         }
349         setInfoField(message, "name", chatterName);
350
351         Writer recorder = channelBuilder.getRecorder(channel);
352         if (recorder != null) {
353             try {
354                 recorder.write(chatterName + " : " + body + "\n");
355             } catch (IOException e) {
356                 log.error("" + e);
357             }
358         }
359
360         /* Make the relation with the channel in which this Message get listed.
361          * And make the relation between message and the user who posted the message.
362          */

363         int result = POST_OK;
364         try {
365             String JavaDoc tmp = tmpNodeManager.createTmpRelationNode("parent", messageUser, getNewTemporaryKey(), "realchannel", key);
366             tmpNodeManager.setObjectField(messageUser, tmp, "snumber", new Integer JavaDoc(channel));
367             // add the message relation to the relation breaker
368
chatboxMessages.add(messageUser + "_" + tmp, (new Long JavaDoc(System.currentTimeMillis() + expireTime)).longValue());
369         } catch (Exception JavaDoc e) {
370             result = POST_ERROR_RELATION_CHANNEL;
371         }
372         if (chatter != -1) {
373             try {
374                 String JavaDoc tmp = tmpNodeManager.createTmpRelationNode("creator", messageUser, getNewTemporaryKey(), "realuser", key);
375                 tmpNodeManager.setObjectField(messageUser, tmp, "snumber", new Integer JavaDoc(chatter));
376                 // add the message relation to the relation breaker
377
chatboxMessages.add(messageUser + "_" + tmp, (new Long JavaDoc(System.currentTimeMillis() + expireTime)).longValue());
378                 MMObjectNode node = tmpNodeManager.getNode(messageUser, tmp);
379                 if (log.isDebugEnabled()) {
380                     log.debug("just set " + tmp + " snumber to " + node.getIntValue("snumber"));
381                 }
382             } catch (Exception JavaDoc e) {
383                 result = POST_ERROR_RELATION_USER;
384             }
385         }
386         // add the message itself to the relation breaker
387
chatboxMessages.add(messageUser + "_" + key, (new Long JavaDoc(System.currentTimeMillis() + expireTime)).longValue());
388         return result;
389     }
390
391     /**
392      * Inserts a message node to the database.
393      *
394      * @param owner The owner of the node.
395      * @param node The node to insert.
396      */

397     public int insert(String JavaDoc owner, MMObjectNode node) {
398         // determine how the timestamp is stored (as a long or as two integers)
399

400         boolean useTimeStamp = (getField(F_TIMESTAMP) != null);
401         if (useTimeStamp) {
402             node.setValue(F_TIMESTAMP, new Long JavaDoc(System.currentTimeMillis()));
403         } else {
404             TimeStamp timeStamp = new TimeStamp();
405             node.setValue("timestampl", timeStamp.lowIntegerValue());
406             node.setValue("timestamph", timeStamp.highIntegerValue());
407         }
408
409         InsRel insrel = mmb.getInsRel();
410         insrel.deleteRelationCache(node.getIntValue(F_THREAD));
411         return (super.insert(owner, node));
412     }
413
414     /**
415      * Changes the subject and body fields, and the name of the poster when stored in the info-field
416      * of an already existing message.
417      * This function is usefull for editing or moderating the context of a message.
418      *
419      * @param chatterName The name of the person who has written the message.
420      * @param subject The subject of the message.
421      * @param body The body of the message.
422      * @param number The message node's number.
423      * @return {@link #POST_OK} on success, otherwise an error number.
424      * The number can be used with getPostError() to get the errormessage.
425      */

426     public int update(String JavaDoc chatterName, String JavaDoc subject, String JavaDoc body, int number) {
427         return update(chatterName, -1, subject, body, number);
428     }
429
430     /**
431      * Changes the subject and body fields, and the name of the poster when stored in the info-field
432      * of an already existing message.
433      * This function is usefull for editing or moderating the context of a message.
434      *
435      * @param chatterName The name of the person who has written the message.
436      * @param chatter the number of the chatter object
437      * @param subject The subject of the message.
438      * @param body The body of the message.
439      * @param number The message node's number.
440      * @return {@link #POST_OK} on success, otherwise an error number.
441      * The number can be used with getPostError() to get the errormessage.
442      */

443     public int update(String JavaDoc chatterName, int chatter, String JavaDoc subject, String JavaDoc body, int number) {
444         if (body.length() == 0) {
445             return POST_ERROR_NO_BODY_TEXT;
446         }
447         if (body.length() > maxBodySize) {
448             return POST_ERROR_BODY_EXCEEDS_SIZE;
449         }
450
451         MMObjectNode node = getNode(number);
452         Vector channels = node.getRelatedNodes("channel");
453         if (channels.size() > 0) {
454             MMObjectNode channelNode = (MMObjectNode) channels.get(0);
455             // test write login
456
// if write-login is true, and no 'chatter' node is specified,
457
// the user has not logged on - so the post should fail
458
if ((channelNode.getIntValue(Channel.F_STATE) & Channel.STATE_WRITE_LOGIN) > 0) {
459                 if (chatter == -1)
460                     return POST_ERROR_NEED_LOGIN;
461             }
462         }
463         log.info("Message:CHATTERNAME=" + chatterName);
464         if (chatterName != null) {
465             log.info("Message:Info pre=" + node.getStringValue("info"));
466             setInfoField(node, "name", chatterName);
467             log.info("Message:Info post=" + node.getStringValue("info"));
468         }
469         node.setValue(F_SUBJECT, subject);
470         node.setValue(F_BODY, body);
471         if (node.commit())
472             return POST_OK;
473         else
474             return POST_ERROR_UNKNOWN;
475     }
476
477     /**
478      * Retrieves a list of messages related to a thread.
479      * The tagger parameter specified what fields to treturn, the order of
480      * the messages, and any filters and options to use for the list.
481      * <br />
482      * Attributes that can be set for th list are:
483      * <ul>
484      * <li>NODE : the number of the message or channel that is the parent of
485      * the messages requested.</li>
486      * <li>FIELDS : a Vector with fieldnames to return. This may change as some
487      * fields are mandatory in certain situations.</li>
488      * <li>ITEMS : this is filled by this method with the actual number of
489      * fields that are returned</li>
490      * <li>FROMCOUNT : starts messsages after the specified number of messages
491      * in the list that results form this query.</li>
492      * <li>MAXCOUNT : Maximum number of messages to return</li>
493      * <li>STARTAFTERNODE : starts messages after the message identified
494      * with the specified node number. Does not work for chats.</li>
495      * <li>STARTAFTERSEQUENCE: starts messages after the message identified
496      * with the specified sequence number.</li>
497      * <li>MAXDEPTH</li> the maximum depth at which to search for (replies to)
498      * messages</li>
499      * <li>SORTFIELDS or DBSORT : the fields to sort on </li>
500      * <li>SORTDIRS or DBDIR : the direction to sort the fields on (UP or DOWN)</li>
501      * <li>OPENTAG : a tag to use instead of <code>&lt;ul&gt;<code> for the
502      * <code>listhead</code> field. Should be tagname without the tag delimiters.</li>
503      * <li>CLOSETAG : a value to use instead of <code>&lt;/ul&gt;<code> for the
504      * <code>listtail</code> field. Should be tagname without the tag delimiters.</li>
505      * </ul>
506      *
507      * @param params the attributes of the LIST tag.
508      * @return A <code>Vector</code> containing the requested fields.
509      */

510     public Vector getListMessages(StringTagger params) {
511         /* Get the thread/node from who the related messages have to be given.
512          */

513         String JavaDoc id = params.Value("NODE");
514         MMObjectNode node = getNode(id);
515         if (node == null) {
516             log.debug("getListMessages(): no or incorrect node specified");
517             return new Vector();
518         }
519
520         /* Get the fieldnames out of the FIELDS attribute.
521          */

522         Vector fields = params.Values("FIELDS");
523
524         /*
525          * When fields contains listhead it's assumed a <ul> HTML listing has to get generated.
526          * A <ul> list can only be generated when listhead, listtail and depth are both used.
527          * Since depth is optional it's added automatically when absent.
528          * OPENTAG and CLOSETAG can contain alternate tags for the <ul> tag(s).
529          */

530         String JavaDoc openTag = LIST_HEAD_TAG;
531         String JavaDoc closeTag = LIST_TAIL_TAG;
532         int listheadItemNr = fields.indexOf("listhead");
533         int listtailItemNr = -1;
534         int depthItemNr = -1;
535         if (listheadItemNr >= 0) {
536             listtailItemNr = fields.indexOf("listtail");
537             depthItemNr = fields.indexOf("depth");
538             // add depth to fiel;ds
539
if (depthItemNr < 0) {
540                 fields.add("depth");
541                 depthItemNr = fields.indexOf("depth");
542             }
543             openTag = params.Value("OPENTAG");
544             closeTag = params.Value("CLOSETAG");
545             if ((openTag == null) || (closeTag == null)) {
546                 openTag = LIST_HEAD_TAG;
547                 closeTag = LIST_TAIL_TAG;
548             } else {
549                 openTag = "<" + openTag.replace('\'', '"').replace('#', '=') + ">";
550                 closeTag = "</" + closeTag + ">";
551             }
552         }
553
554         // Put in params the number of fields that will get returned.
555
params.setValue("ITEMS", "" + fields.size());
556
557         // Get fromCount and maxCount.
558
String JavaDoc tmp = params.Value("FROMCOUNT");
559         int fromCount;
560         if (tmp != null) fromCount = Integer.decode(tmp).intValue(); else fromCount = 0;
561         int maxCount;
562         tmp = params.Value("MAXCOUNT");
563         // MAXCOUNT was maxCount (now really!) line below is to allow support for 'old'
564
// communities, but should be dropped!
565
if (tmp == null) tmp = params.Value("maxCount");
566
567         if (tmp != null) maxCount = Integer.decode(tmp).intValue(); else maxCount = Integer.MAX_VALUE;
568         int maxDepth;
569         tmp = params.Value("MAXDEPTH");
570         if (tmp != null) maxDepth = Integer.decode(tmp).intValue(); else maxDepth = Integer.MAX_VALUE;
571
572         // Get startAfterNode / startAfterSequence
573
String JavaDoc nodeselectfield = "number";
574         int startAfterNode = -1;
575
576         tmp = params.Value("STARTAFTERNODE");
577         try {
578             if (tmp != null) {
579                 startAfterNode = Integer.decode(tmp).intValue();
580             } else {
581                 tmp = params.Value("STARTAFTERSEQUENCE");
582                 if (tmp != null) {
583                     startAfterNode = Integer.decode(tmp).intValue();
584                     nodeselectfield = F_SEQUENCE;
585                 }
586             }
587         } catch (NumberFormatException JavaDoc e) {
588             log.error("" + e);
589         }
590
591         /* Create a NodeComparator. If no SORTFIELDS or DBSORT are specified in
592          * the list sequence is used as a default.
593          * Sortdirections can be specified in SORTDIRS or DBDIR.
594          */

595         Vector sortFields = params.Values("SORTFIELDS");
596         if (sortFields == null) {
597             sortFields = params.Values("DBSORT");
598             if (sortFields == null) {
599                 sortFields = new Vector(1);
600                 sortFields.add(F_SEQUENCE);
601             }
602         }
603         Vector sortDirs = params.Values("SORTDIRS");
604         if (sortDirs == null) sortDirs = params.Values("DBDIR");
605         NodeComparator compareMessages;
606         if (sortDirs == null) {
607             compareMessages = new NodeComparator(sortFields);
608         } else {
609             compareMessages = new NodeComparator(sortFields, sortDirs);
610         }
611
612         Vector result = null;
613
614         if (maxCount == Integer.MAX_VALUE) {
615             // no max, grab everything
616
result = getListMessages(node, fields, compareMessages, maxCount, 0,
617                     maxDepth, startAfterNode, nodeselectfield);
618         } else {
619             // limit list to given max of items
620
result = getListMessages(node, fields, compareMessages, fromCount + maxCount, 0,
621                     maxDepth, startAfterNode, nodeselectfield);
622             int realCount = (fromCount + maxCount) * fields.size();
623             int realFromCount = (fromCount) * fields.size();
624             if (result.size() > realCount) {
625                 result = new Vector(result.subList(realFromCount, realCount));
626             } else if (realFromCount > 0) {
627                 if (realFromCount >= result.size()) {
628                     result = new Vector();
629                 } else {
630                     result = new Vector(result.subList(realFromCount, result.size()));
631                 }
632             }
633         }
634
635         if ((listheadItemNr >= 0) && (listtailItemNr >= 0)) {
636             addListTags(result, listheadItemNr, listtailItemNr, depthItemNr, fields.size(), openTag, closeTag);
637         } else if ((listheadItemNr >= 0) || (listtailItemNr >= 0)) {
638             log.error("getListMessages(): Listhead and listtail only work when used together.");
639         }
640         return result;
641     }
642
643     /**
644      * Retrieves a list of messages related to a thread.
645      * @param thread the number of the message or channel that is the parent of
646      * the messages requested
647      * @param fields a Vector with fieldnames to return. This may change as some
648      * fields are mandatory in certain situations
649      * @param ci A Comparator to use for sorting the messages
650      * @param maxCount : Maximum number of messages to return</li>
651      * @param depth the current depth at which emssages are searched
652      * @param maxDepth the maximum depth at which to search for (replies to)
653      * messages
654      * @param startAfterNode starts messages after the message identified
655      * with the specified value
656      * @param nodeselectfield field to which to compare the value of
657      * startAfterNode (i.e. 'number' or 'sequence').
658      * @return A <code>Vector</code> containing the requested fields.
659      */

660     public Vector getListMessages(MMObjectNode thread, Vector fields, Comparator ci,
661                                   int maxCount, int depth, int maxDepth, int startAfterNode,
662                                   String JavaDoc nodeselectfield) {
663         Vector result = new Vector(), childMsgs;
664         Vector relatedMessages = getRelatedMessages(thread, ci);
665
666         int msgPointer = relatedMessages.size() - 1;
667         int added = 0, count = 0;
668         String JavaDoc item, cmd;
669         MMObjectNode relmsg;
670         int fieldsCount = fields.size();
671         int replycount;
672         boolean startAfterNodePassed = (startAfterNode == -1);
673
674         while ((msgPointer >= 0) && (added < maxCount)) {
675             relmsg = (MMObjectNode) relatedMessages.elementAt(msgPointer);
676
677             startAfterNodePassed = startAfterNodePassed ||
678                     (startAfterNode == relmsg.getIntValue(nodeselectfield));
679
680             if (depth < maxDepth) {
681                 childMsgs = getListMessages(relmsg, fields, ci, maxCount - added, depth + 1,
682                         maxDepth, startAfterNode, nodeselectfield);
683                 replycount = childMsgs.size() / fieldsCount;
684                 added += replycount;
685
686                 /* When recursion stops because we've got a big enough result,
687                  * the number of returned childMsg is less than the actual ammount of childmessages
688                  * and replycount isn't calculated correctly, therefor getNrMsgAndHighSeq is called.
689                  */

690                 if (added == maxCount) replycount = getNrMsgAndHighSeq(relmsg).messageCount;
691             } else {
692                 childMsgs = new Vector();
693                 replycount = getNrMsgAndHighSeq(relmsg).messageCount;
694             }
695
696             if (startAfterNodePassed) {
697
698                 added++; // Another message is going to be added.
699

700                 /* Add the fields of the relmsg to the result and then of his children.
701                 */

702                 for (int i = 0; i < fieldsCount; i++) {
703                     cmd = ((String JavaDoc) fields.elementAt(i)).trim();
704                     if (cmd.equals("depth")) {
705                         item = "" + depth;
706                     } else if (cmd.equals("replycount")) {
707                         item = "" + replycount;
708                     } else if ((cmd.equals("listhead")) ||
709                             (cmd.equals("listtail"))) {
710                         item = "";
711                     } else {
712                         item = "" + relmsg.getValue(cmd);
713                     }
714                     result.add(item);
715                 }
716                 result.addAll(childMsgs);
717             }
718             msgPointer--;
719             count++;
720         }
721         return result;
722     }
723
724     /**
725      * Retrieves a list of messages related to a thread.
726      * @param node the message or channel node that is the parent of
727      * the messages requested
728      * @param ci A Comparator to use for sorting the messages
729      * @return A <code>Vector</code> containing the requested message nodes.
730      */

731     public Vector getRelatedMessages(MMObjectNode node, Comparator ci) {
732         // we might decide to use a SortedSet instead
733
Vector result = new Vector();
734         Enumeration relatedMessages;
735
736         MMObjectNode channel = isPostedInChannel(node);
737         MMObjectNode community = channelBuilder.communityParent(channel);
738         String JavaDoc kind;
739         if (community != null) {
740             kind = (String JavaDoc) community.getValue("kind");
741         } else {
742             kind = "forum";
743         }
744
745         if (kind.equalsIgnoreCase("chatbox")) {
746             relatedMessages = getTemporaryRelated(node, "message").elements();
747         } else {
748             relatedMessages = getReplies(node);
749         }
750
751         if (relatedMessages == null) return result;
752
753         MMObjectNode relmsg = null;
754         int parent = node.getNumber();
755         while (relatedMessages.hasMoreElements()) {
756             // XXX: Tempory hack: test if the related message is indeed
757
// a child by his thread field.
758
relmsg = (MMObjectNode) relatedMessages.nextElement();
759             if (relmsg.getIntValue(F_THREAD) == parent)
760                 result.add(relmsg);
761         }
762         Collections.sort(result, ci);
763         return result;
764     }
765
766     /**
767      * Add a node to a result vector, provided the type matches and the
768      * current count is higher than the offset of the resultlist.
769      */

770     private int addRelatedNode(MMObjectNode node, int otypeWanted, int count, int offset, Vector result) {
771         if ((node != null) &&
772                 (node.getIntValue("otype") == otypeWanted)) {
773             count += 1;
774             if (count > offset) {
775                 result.add(node);
776             }
777         }
778         return count;
779     }
780
781     /**
782      * Add a node identified by number to a result vector,
783      * provided the type matches and the current count is higher than the
784      * offset of the resultlist.
785      */

786     private int addRelated(int number, int otypeWanted, int count, int offset, Vector result) {
787         MMObjectNode node = getNode(number);
788         return addRelatedNode(node, otypeWanted, count, offset, result);
789     }
790
791     /**
792      * Add a temporary node identified by key to a result vector,
793      * provided the type matches and the current count is higher than the
794      * offset of the resultlist.
795      */

796     private int addRelated(Object JavaDoc key, int otypeWanted, int count, int offset, Vector result) {
797         if (key != null) {
798             MMObjectNode node = (MMObjectNode) temporaryNodes.get("" + key);
799             count = addRelatedNode(node, otypeWanted, count, offset, result);
800         }
801         return count;
802     }
803
804     /**
805      * Get temporary MMObjectNodes related to a specified MMObjectNode
806      * @param node this is the source MMObjectNode
807      * @param wtype Specifies the type of the nodes you want to have e.g. wtype="pools"
808      */

809     public Vector getTemporaryRelated(MMObjectNode node, String JavaDoc wtype) {
810         return getTemporaryRelated(node, wtype, 0, Integer.MAX_VALUE);
811     }
812
813     /**
814      * Get temporary MMObjectNodes related to a specified MMObjectNode
815      * @param node this is the source MMObjectNode
816      * @param wtype Specifies the type of the nodes you want to have e.g. wtype="pools"
817      */

818     public Vector getTemporaryRelated(MMObjectNode node, String JavaDoc wtype,
819                                       int offset, int max) {
820         Vector result = new Vector();
821         if (max <= 0) return result;
822         MMObjectNode tmpInsRel;
823         boolean found;
824         int otypewanted = mmb.getTypeDef().getIntValue(wtype);
825         int count = 0;
826
827         // Get the node's number or _number.
828
int number = -1;
829         String JavaDoc _number = (String JavaDoc) node.getValue("_number");
830         if (_number == null) number = node.getNumber();
831
832         // Get all temporary nodes and filter out all insrels.
833
Iterator tmpInsRels = temporaryNodes.keySet().iterator();
834         while ((count < (offset + max)) && tmpInsRels.hasNext()) {
835             tmpInsRel = (MMObjectNode) temporaryNodes.get(tmpInsRels.next());
836             if (tmpInsRel != null) {
837                 if (tmpInsRel.getBuilder() instanceof InsRel) {
838                     found = false;
839                     // Test if the (_)snumbers are equal.
840
if (_number != null) {
841                         found = _number.equals(tmpInsRel.getStringValue("_snumber"));
842                     } else {
843                         found = (number == tmpInsRel.getIntValue("snumber"));
844                     }
845                     if (found) { // snumbers are equal
846
int dnumber = tmpInsRel.getIntValue("dnumber");
847                         if (dnumber > -1) {
848                             count = addRelated(dnumber, otypewanted, count, offset, result);
849                         } else {
850                             count = addRelated(tmpInsRel.getValue("_dnumber"),
851                                     otypewanted, count, offset, result);
852                         }
853                     } else {
854                         if (_number != null) {
855                             found = _number.equals(tmpInsRel.getStringValue("_dnumber"));
856                         } else {
857                             found = (number == tmpInsRel.getIntValue("dnumber"));
858                         }
859                         if (found) { // (_)dumbers are equal.
860
int snumber = tmpInsRel.getIntValue("snumber");
861                             if (snumber > -1) {
862                                 count = addRelated(snumber, otypewanted, count, offset, result);
863                             } else {
864                                 count = addRelated(tmpInsRel.getValue("_snumber"),
865                                         otypewanted, count, offset, result);
866                             }
867                         }
868                     }
869                 }
870             }
871         }
872         return result;
873     }
874
875     /**
876      * Adds the values of <code>listhead</code> and <code>listtail</code> to a
877      * list of fields.
878      * There was no easy way for letting getListMessages() fill in the virtual
879      * fields <code>listhead</code> and <code>listtail</code>, so it's done
880      * here instead.
881      * @param fields contains a list of message field values, ordered by message and fieldnumber.
882      * @param listheadItemNr the field number of the <code>listhead</code> field
883      * @param listtailItemNr the field number of the <code>listtail</code> field
884      * @param depthItemNr the field number of the <code>depth</code> field
885      * @param fieldscount teh numbe rof fields per message stored in fields
886      * @param opentag the base value to enter in <code>listhead</code> when a
887      * message is the first in a thread
888      * @param closetag the base value to enter in <code>listhead</code> when
889      * a message follows a thread or to <code>listtail</code> when a
890      * message is the last in the list.
891      */

892     private void addListTags(Vector items, int listheadItemNr, int listtailItemNr,
893                              int depthItemNr, int fieldsCount, String JavaDoc openTag, String JavaDoc closeTag) {
894         int depth;
895         int previousDepth = 0;
896         int itemNr = 0;
897         int itemsCount = items.size();
898         String JavaDoc prefix, postfix;
899         while (itemNr < itemsCount) {
900             depth = (Integer.decode((String JavaDoc) items.elementAt(itemNr + depthItemNr))).intValue();
901             if (itemNr == 0) {
902                 prefix = openTag; // begin of the list
903
while (previousDepth < depth) {
904                     prefix += openTag; // begin of sublist, just in case a page starts with a sublist
905
previousDepth++;
906                 }
907             } else if (depth > previousDepth) {
908                 prefix = openTag; // begin of a sublist
909
} else {
910                 prefix = "";
911             }
912             postfix = "";
913             if (depth < previousDepth) {
914                 while (previousDepth > depth) {
915                     prefix += closeTag; // end of one ore more sublists
916
previousDepth--;
917                 }
918             }
919             if (itemNr == itemsCount - fieldsCount) {
920                 while (depth > 0) {
921                     postfix += closeTag; // end of one ore more sublists
922
depth--;
923                 }
924                 postfix += closeTag;
925             }
926             items.set(itemNr + listheadItemNr, prefix);
927             items.set(itemNr + listtailItemNr, postfix);
928             itemNr += fieldsCount;
929             previousDepth = depth;
930         }
931     }
932
933     /**
934      * Remove a node if it's not a parent message to another message.
935      * If it is, the node is not removed.
936      * @param node the node to remove
937      */

938     public void removeNode(MMObjectNode node) {
939         removeNode(node, false);
940     }
941
942     /**
943      * Remove a node and, optionally, it's child nodes.
944      * @param node the node to remove
945      * @param recursive if <code>true</code>, the node will be removed along with
946      * its childnodes if it has any. Otherwise, it will only be removed if the ndoe
947      * hs no children.
948      *
949      */

950     public void removeNode(MMObjectNode node, boolean recursive) {
951         log.service("removeNode(): node=" + node.getNumber());
952
953         // Make sure we got a Message node, else abort the remove operation.
954
if (!(node.getBuilder() instanceof Message)) {
955             log.error(node.getNumber() + " is of type " + node.getName() + " instead of Message");
956             return;
957         }
958
959         // If recursive is true delete the childmessages
960
// else abort on any childmessages.
961
MMObjectNode relmsg;
962         for (Enumeration messages = getReplies(node); messages.hasMoreElements();) {
963             relmsg = (MMObjectNode) messages.nextElement();
964             // Before doing all this hot stuff, there should be
965
// a check preventing recorded messages get deleted!
966
// (not an issue right now, as session is not implemented)
967
if (relmsg.getIntValue(F_THREAD) == node.getNumber()) {
968                 // Here we have a childmessage.
969
if (recursive) {
970                     removeNode(relmsg, true);
971                 } else {
972                     log.error("Can't delete Message " + node.getNumber() + " because it has child Messages.");
973                     return;
974                 }
975             }
976         }
977         // Remove all relations to other nodes.
978
InsRel insrel = mmb.getInsRel();
979         insrel.removeRelations(node);
980         if (node.hasRelations()) {
981             log.error("Failed to remove node relations");
982         }
983         // Now it's possible to remove the node itself.
984
super.removeNode(node);
985     }
986
987     public ThreadStats getNrMsgAndHighSeq(MMObjectNode node) {
988         /* PRE: A message or channel node.
989          * POST: A ThreadStats object which contains the number of messages
990          * and the highest sequence in this thread.
991          */

992         int sequence = -1;
993         int highestSequence = -1;
994         int messageCount = 0;
995         MMObjectNode msg;
996         for (Enumeration messages = getReplies(node); messages.hasMoreElements();) {
997             msg = (MMObjectNode) messages.nextElement();
998             if (msg.getIntValue(F_THREAD) == node.getNumber()) {
999                 sequence = msg.getIntValue(F_SEQUENCE);
1000                if (sequence > highestSequence) highestSequence = sequence;
1001                /* Get the nummer of messages and the highes sequence in this childmessage.
1002                 */

1003                ThreadStats cs = getNrMsgAndHighSeq(msg);
1004                messageCount += cs.messageCount + 1;
1005                if (cs.highestSequence > highestSequence) highestSequence = cs.highestSequence;
1006            }
1007        }
1008        return new ThreadStats(node.getNumber(), messageCount, highestSequence);
1009    }
1010
1011    /**
1012     * Returns whether a node has replies or not.
1013     * @param node the node to find the relations of
1014     * @return <code>true</code> if the node has related messages.
1015     */

1016    public boolean hasReplies(MMObjectNode node) {
1017        return getNrMsgAndHighSeq(node).messageCount > 0;
1018    }
1019
1020    /**
1021     * Get all relations for a node with other messages.
1022     * These are not necessarily all a reply to this message (it may be a
1023     * parent message).
1024     * XXX: should be fixed with a directional modifier, but then we first need
1025     * a few additonal calls in InsRel or MMObjectNode.
1026     * @param node the node to find the relations of
1027     * @return an <code>Enumeration</code> containign the related messages.
1028     */

1029    public Enumeration getReplies(MMObjectNode node) {
1030        return node.getRelatedNodes("message").elements();
1031    }
1032
1033    /**
1034     * Provides additional functionality when obtaining field values.
1035     * This method is called whenever a Node of the builder's type fails at
1036     * evaluating a getValue() request (generally when a fieldname is supplied
1037     * that doesn't exist).
1038     * <br />
1039     * The following virtual fields are calculated:
1040     * <br />
1041     * timestamp - returns the timestamp of the message
1042     * <br />
1043     * resubject - returns the subject with a "RE:" prefix (if needed)
1044     * <br />
1045     * replycount - returns the number of replies
1046     * <br />
1047     * hasreplies - returns "true" if the message has replies, "false" otherwise
1048     * <br />
1049     * parent - returns the number of the parent node (same as "thread")
1050     *
1051     * @param node the node whose fields are queried
1052     * @param field the fieldname that is requested
1053     * @return the result of the call, <code>null</code> if no valid functions or virtual fields could be determined.
1054     */

1055    public Object JavaDoc getValue(MMObjectNode node, String JavaDoc field) {
1056        // if we get here, timestamp is NOT an existing field,
1057
// so retrieve the values using the two integer fields
1058
if (field.equals(F_TIMESTAMP)) {
1059            TimeStamp ts = new TimeStamp(); // default, return current time
1060
if (super.getValue(node, F_TIMESTAMP) == null) { // no value to be found.
1061
if (getField(F_TIMESTAMP) == null) { // field does not exist
1062
ts = getTimeStamp(node);
1063                }
1064            }
1065            return "" + ts.getTime();
1066        }
1067        if (field.equals(F_TIMESTAMPSEC)) {
1068            long ts = node.getLongValue(F_TIMESTAMP) / 1000;
1069            return new Long JavaDoc(ts);
1070        }
1071        if (field.equals(F_RE_SUBJECT)) {
1072            String JavaDoc subject = node.getStringValue(F_SUBJECT);
1073            if (subject.startsWith("RE: ")) return subject; else return ("RE: " + subject);
1074        }
1075        if (field.equals(F_REPLY_COUNT)) {
1076            return new Integer JavaDoc(getNrMsgAndHighSeq(node).messageCount);
1077        }
1078        if (field.equals(F_HAS_REPLIES)) {
1079            return Boolean.valueOf(hasReplies(node));
1080        }
1081        if (field.equals(F_PARENT)) {
1082            return node.getValue(F_THREAD);
1083        }
1084        // XXX: substring is supported for now, but should be dropped as it already
1085
// exists in MMObjectBuilder.
1086
// the main difference is that MMObjectBuilder separates values with
1087
// comma or semicolon, while this version seperates using 'x'.
1088
if (field.startsWith("substring")) {
1089            /* Something like substring(realfield,size) is expected. The realField is asked from the node and
1090             * truncated to the size. Truncation is done before whole words.
1091             */

1092            int commaPos = field.lastIndexOf('x');
1093            String JavaDoc realField = field.substring(10, commaPos); // 10 is the beginning of the first character after '('.
1094
String JavaDoc value = (String JavaDoc) node.getValue(realField);
1095            int size = Integer.decode(field.substring(commaPos + 1, field.length() - 1)).intValue();
1096            if (size < value.length())
1097                return value.substring(0, size);
1098            else
1099                return value;
1100        } // ??? Must still cut for new word!
1101

1102        Object JavaDoc o = super.getValue(node, field);
1103        if (o instanceof String JavaDoc) {
1104            if ((field.indexOf("html(") < 0) && (field.indexOf("html_") < 0))
1105                o = getHTML((String JavaDoc) o);
1106        }
1107        return o;
1108    }
1109
1110    /**
1111     * Executes a function on the field of a node, and returns the result.
1112     * This method is called by the builder's {@link #getValue} method.
1113     * Functions implemented are hasreplies() and getinfovalue().
1114     * @param node the node whose fields are queries
1115     * @param field the fieldname that is requested
1116     * @return the result of the 'function', or null if no valid functions could be determined.
1117     */

1118    protected Object JavaDoc executeFunction(MMObjectNode node, String JavaDoc function, String JavaDoc field) {
1119        if (function.equals(F_GET_INFO_VALUE)) {
1120            return getInfoField(node, field);
1121        } else {
1122            return super.executeFunction(node, function, field);
1123        }
1124    }
1125
1126    /**
1127     * Handles the $MOD-MMBASE-BUILDER-message- commands.
1128     */

1129    public String JavaDoc replace(PageInfo sp, StringTokenizer tok) {
1130        /* The first thing we expect is a message number.
1131         */

1132        if (!tok.hasMoreElements()) {
1133            log.error("replace(): message number expected after $MOD-BUILDER-message-.");
1134            return "";
1135        }
1136        String JavaDoc tmp = tok.nextToken();
1137        MMObjectNode message = getNode(tmp);
1138        //tmp = tmp.substring(tmp.indexOf("_") + 1);
1139
if (message == null) message = (MMObjectNode) temporaryNodes.get(tmp.trim());
1140        if (message == null) {
1141            log.error("Message with id '" + tmp + "' cannot be found.");
1142            return "";
1143        }
1144
1145        if (tok.hasMoreElements()) {
1146            String JavaDoc cmd = tok.nextToken();
1147            if (cmd.equals("DEL")) removeNode(message, true);
1148            if (cmd.equals("GETINFOFIELD")) return getInfoField(message, tok.nextToken());
1149            if (cmd.equals("SETINFOFIELD")) {
1150                setInfoField(message, tok.nextToken(), tok.nextToken());
1151                message.commit();
1152            }
1153        }
1154        return "";
1155    }
1156
1157    /**
1158     * Set a value in the multi-purpose <code>info</code> field.
1159     * The info field of the message node contains a StringTagger.
1160     * This function sets the field in the tagger to value.
1161     * @param message the node in which to store the data
1162     * @param field the name of the value as it is to be stored in <code>info</code>
1163     * @param value the value to store
1164     */

1165    private void setInfoField(MMObjectNode message, String JavaDoc field, String JavaDoc value) {
1166        value = remove(value, "\n=,\"");
1167        String JavaDoc info = message.getStringValue(F_INFO);
1168        StringTagger tagger = new StringTagger(info, '\n', '=', ',', '\"');
1169        tagger.setValue(field, value);
1170        String JavaDoc key = "";
1171        String JavaDoc content = "";
1172        for (Iterator i = tagger.keySet().iterator(); i.hasNext();) {
1173            key = (String JavaDoc) i.next();
1174            content += key + "=" + tagger.Value(key) + "\n";
1175        }
1176        message.setValue(F_INFO, content);
1177    }
1178
1179    private String JavaDoc remove(String JavaDoc s, String JavaDoc r) {
1180        if (s == null) return null;
1181        String JavaDoc result = "";
1182        for (int i = 0; i < s.length(); i++) {
1183            char c = s.charAt(i);
1184            if (r.indexOf(c) < 0)
1185                result = result + c;
1186        }
1187        return result;
1188    }
1189
1190    /**
1191     * Obtain a value from the multi-purpose <code>info</code> field.
1192     * The info field of the message node contains a StringTagger.
1193     * This function gets the value out of the field in the tagger.
1194     * @param message the node from which to obtain the data
1195     * @param field the name of the value as it is known in <code>info</code>
1196     * @return the retrieved value as a <code>String</code>
1197     */

1198    private String JavaDoc getInfoField(MMObjectNode message, String JavaDoc field) {
1199        String JavaDoc info = message.getStringValue(F_INFO);
1200        StringTagger tagger = new StringTagger(info, '\n', '=', ',', '\"');
1201        return getHTML(tagger.Value(field));
1202    }
1203
1204    /**
1205     * Get the nodes timestampl and timestamph and return them as a TimeStamp.
1206     */

1207    public TimeStamp getTimeStamp(MMObjectNode node) {
1208        return new TimeStamp((Integer JavaDoc) node.getValue("timestampl"), (Integer JavaDoc) node.getValue("timestamph"));
1209    }
1210
1211    /**
1212     * Returns the channel in which the given message node is posted.
1213     * @param node the node to get the channel of
1214     * @return the channel of the message as an <code>MMObjectNode</code>
1215     */

1216    public MMObjectNode isPostedInChannel(MMObjectNode node) {
1217        while (node.getBuilder() instanceof Message) {
1218            node = getNode(node.getIntValue(F_THREAD));
1219        }
1220        return node;
1221    }
1222
1223    /**
1224     * Generates a new temporary key for a temporary message.
1225     */

1226    synchronized public String JavaDoc getNewTemporaryKey() {
1227        return (tmpNumberPrefix + tmpNumbers++);
1228    }
1229}
1230
1231/**
1232 * Class for holding statistics on a thread.
1233 */

1234class ThreadStats {
1235    /** The thread (channel or message number) to which these stats belong */
1236    public int thread = -1;
1237    /** The number of messages in this thread */
1238    public int messageCount = 0;
1239    /** The highest sequence number in this thread */
1240    public int highestSequence = 0;
1241
1242    /**
1243     * Create a statistics object for a thread
1244     * @param thread the thread (channel or message number) to which these stats belong
1245     * @param messageCount The number of messages in this thread
1246     * @param highestsequence The highest sequence number in this thread
1247     */

1248    public ThreadStats(int thread, int messageCount, int highestSequence) {
1249        this.thread = thread;
1250        this.messageCount = messageCount;
1251        this.highestSequence = highestSequence;
1252    }
1253}
1254
Popular Tags