mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-05 08:35:26 +00:00
541 lines
18 KiB
Java
541 lines
18 KiB
Java
/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*-
|
|
*
|
|
* The contents of this file are subject to the Mozilla Public License
|
|
* Version 1.0 (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 Grendel mail/news client.
|
|
*
|
|
* The Initial Developer of the Original Code is Netscape Communications
|
|
* Corporation. Portions created by Netscape are Copyright (C) 1997
|
|
* Netscape Communications Corporation. All Rights Reserved.
|
|
*
|
|
* Created: Jamie Zawinski <jwz@netscape.com>, 1 Dec 1997.
|
|
*/
|
|
|
|
package grendel.storage;
|
|
|
|
import calypso.util.ByteBuf;
|
|
import calypso.util.ByteLineBuffer;
|
|
import calypso.util.Assert;
|
|
|
|
import java.util.Vector;
|
|
|
|
import java.io.InputStream;
|
|
import java.io.IOException;
|
|
|
|
import javax.mail.FetchProfile;
|
|
import javax.mail.Folder;
|
|
import javax.mail.Message;
|
|
import javax.mail.MessagingException;
|
|
import javax.mail.Store;
|
|
import javax.mail.event.MessageChangedEvent;
|
|
import javax.mail.event.MessageCountEvent;
|
|
|
|
|
|
/** This class implements a Folder representing a newsgroup.
|
|
*/
|
|
class NewsFolder extends FolderBase {
|
|
|
|
static final boolean DEBUG = false;
|
|
|
|
private Folder parent;
|
|
private String name;
|
|
private NewsStore store;
|
|
|
|
// Whether the fMessages vector has been initialized.
|
|
private boolean loaded = false;
|
|
|
|
// Cached responses of the GROUP command.
|
|
private int high_message = -1;
|
|
private int low_message = -1;
|
|
private int estimated_message_count = -1;
|
|
|
|
// If true, GROUP gave us an error; this group is probably nonexistant.
|
|
private boolean bogus_group_p = false;
|
|
|
|
|
|
/** The maximum portion of the elements in an NNTP XOVER request which
|
|
are allowed to be superfluous. (Some amount of superfluity is good,
|
|
since it reduces the number of client-server round-trips, at the cost
|
|
of sending slightly more data per request.)
|
|
*/
|
|
static final double ALLOWABLE_XOVER_WASTE = 0.25;
|
|
|
|
/** The maximum number of articles which may be requested per NNTP XOVER
|
|
request. Larger values reduce the number of client-server round-trips;
|
|
smaller values increase client responsiveness on slow connections.
|
|
*/
|
|
static final int MAX_XOVER_REQUEST_SIZE = 50;
|
|
|
|
|
|
NewsFolder(Store s, Folder parent, String name) {
|
|
super(s);
|
|
this.parent = parent;
|
|
this.name = name;
|
|
this.store = (NewsStore) s;
|
|
}
|
|
|
|
public char getSeparator() {
|
|
// #### If we're in "all groups" mode, this will be '.'.
|
|
// Otherwise, there is no hierarchy, and therefore no separator.
|
|
return '\000';
|
|
}
|
|
|
|
public int getType() {
|
|
return HOLDS_MESSAGES;
|
|
}
|
|
|
|
public String getName() {
|
|
return name;
|
|
}
|
|
|
|
public String getFullName() {
|
|
return name;
|
|
}
|
|
|
|
public Folder getParent() {
|
|
return parent;
|
|
}
|
|
|
|
public Folder[] list(String pattern) {
|
|
return null;
|
|
}
|
|
|
|
public Folder getFolder(String subfolder) {
|
|
return null;
|
|
}
|
|
|
|
public boolean create(int type) {
|
|
return false;
|
|
}
|
|
|
|
public boolean exists() {
|
|
return !bogus_group_p;
|
|
}
|
|
|
|
protected void getMessageCounts() {
|
|
int counts[] = store.getGroupCounts(this);
|
|
if (counts == null) {
|
|
bogus_group_p = true;
|
|
} else {
|
|
estimated_message_count = counts[0];
|
|
low_message = counts[1];
|
|
high_message = counts[2];
|
|
}
|
|
}
|
|
|
|
// #### Is "unread" the right sense of "new" for NNTP?
|
|
public boolean hasNewMessages() {
|
|
return (getUnreadMessageCount() > 0);
|
|
}
|
|
|
|
/** Returns the total number of messages in the folder, or -1 if unknown.
|
|
This includes deleted and unread messages.
|
|
*/
|
|
public int getMessageCount() {
|
|
if (bogus_group_p)
|
|
return -1;
|
|
if (estimated_message_count == -1)
|
|
getMessageCounts();
|
|
return estimated_message_count;
|
|
}
|
|
|
|
/** Returns the number of non-deleted messages in the folder, or -1 if
|
|
unknown. This includes unread messages.
|
|
*/
|
|
public int getUndeletedMessageCount() {
|
|
return getMessageCount();
|
|
}
|
|
|
|
/** Returns the number of unread messages in the folder, or -1 if unknown.
|
|
This does not include unread messages that are also deleted.
|
|
*/
|
|
public int getUnreadMessageCount() {
|
|
if (bogus_group_p)
|
|
return -1;
|
|
if (estimated_message_count == -1)
|
|
getMessageCounts();
|
|
|
|
if (low_message == -1 || high_message == -1)
|
|
return -1;
|
|
|
|
NewsRC rc = store.getNewsRC();
|
|
NewsRCLine nl = rc.getNewsgroup(getFullName());
|
|
long unread = nl.countMissingInRange(low_message, high_message+1);
|
|
return (int) unread;
|
|
}
|
|
|
|
|
|
/** Returns the number of bytes consumed by deleted but not expunged
|
|
messages in the folder, or -1 if unknown.
|
|
*/
|
|
long deletedMessageBytes() {
|
|
return 0;
|
|
}
|
|
|
|
|
|
public void appendMessages(Message msgs[]) throws MessagingException {
|
|
throw new MessagingException("can't append messages to newsgroups");
|
|
}
|
|
|
|
public void fetch(Message msgs[], FetchProfile fp) {
|
|
}
|
|
|
|
public Message[] expunge() throws MessagingException {
|
|
return null;
|
|
}
|
|
|
|
public boolean delete(boolean value) {
|
|
// #### this should do cancel
|
|
Assert.NotYetImplemented("NewsFolder.delete()");
|
|
return false;
|
|
}
|
|
|
|
public boolean renameTo(Folder f) {
|
|
// Can't rename newsgroups.
|
|
return false;
|
|
}
|
|
|
|
void ensureLoaded() {
|
|
if (DEBUG)
|
|
System.err.println("NF.ensureLoaded " + name);
|
|
|
|
if (bogus_group_p)
|
|
return;
|
|
|
|
if (loaded)
|
|
return;
|
|
|
|
synchronized (this) {
|
|
if (loaded) // double check
|
|
return;
|
|
|
|
if (estimated_message_count == -1)
|
|
getMessageCounts();
|
|
|
|
NewsRC rc = store.getNewsRC();
|
|
NewsRCLine nl = rc.getNewsgroup(getFullName());
|
|
|
|
long start = (int) nl.firstNonMember();
|
|
long end = high_message;
|
|
|
|
Assert.Assertion(start > 0);
|
|
|
|
if (start < low_message) {
|
|
// There is a message marked as unread in the newsrc file which
|
|
// has expired from the news server (it is below the low water mark.)
|
|
// Mark all messages up to (but not including) the low water mark as
|
|
// read, to reduce the newsrc file size.
|
|
//
|
|
if (DEBUG)
|
|
System.err.println("NEWSRC: low water mark is " + low_message +
|
|
" but first-unread is " + start + ".\n" +
|
|
" marking 1-" + (low_message-1) +
|
|
" as read.");
|
|
nl.insert(1, low_message);
|
|
start = (int) nl.firstNonMember();
|
|
}
|
|
|
|
if (end < start)
|
|
return;
|
|
|
|
|
|
/* We want to get the data for *certain* message between `start'
|
|
and `end' inclusive; but not necessarily *all* of them (there
|
|
might be holes.)
|
|
|
|
We can only do this by requesting ranges.
|
|
|
|
* The fewer unneeded articles we request, the better.
|
|
* The fewer range requests we make, the better.
|
|
* But, keeping the length of the range requested relatively
|
|
short is also good, to avoid hogging the pipe, and giving
|
|
other users of the NNTP connection a chance to have their
|
|
say. In other words, there is a preferred range length.
|
|
|
|
We can request a range that includes articles we don't
|
|
specifically want (the holes.)
|
|
|
|
Algorithm:
|
|
|
|
- Move forward from start to end, accumulating ranges of
|
|
articles to request.
|
|
|
|
- Keep track of how many needed and unneeded articles we
|
|
are requesting.
|
|
|
|
- If adding a needed range (along with its preceeding
|
|
unneeded range) to the set would push the ratio of
|
|
needed/unneeded in the current request past a certain
|
|
threshold, then break the request into two before this
|
|
range: flush out what we've got, skip over the unneeded
|
|
leading range (the unneeded messages before this range of
|
|
needed messages), and start building a new request starting
|
|
with the beginning of this range of needed messages.
|
|
|
|
- Also, if adding a needed range would push the number of
|
|
articles in the request past a certain number, break
|
|
the request at that point: that is, break the current
|
|
range in two.
|
|
*/
|
|
|
|
long req_start = start;
|
|
long total_in = 0;
|
|
long total_out = 0;
|
|
|
|
if (DEBUG)
|
|
System.err.println("NEWSRC: computing XOVER range for " +
|
|
start + "-" + end);
|
|
|
|
long i = start;
|
|
long last_read = nl.max();
|
|
|
|
while (i <= end) {
|
|
long range_start, range_end; // this block of unread messages
|
|
|
|
// Count up and skip over the following range of already-read
|
|
// messages. That is, the already-read messages that lie before
|
|
// this range, and after the previous range.
|
|
//
|
|
range_start = (i >= last_read
|
|
? i
|
|
: nl.firstNonMember(i, last_read+1));
|
|
if (range_start == -1)
|
|
range_start = i;
|
|
|
|
if (range_start > end)
|
|
break;
|
|
|
|
// Count up and skip over the following range of unread messages.
|
|
// That is, the unread messages lying after the read messages we
|
|
// skipped above.
|
|
//
|
|
range_end = nl.nextMember(range_start) - 1;
|
|
if (range_end < 0 || range_end > end)
|
|
range_end = end;
|
|
|
|
if ((range_end - req_start) >= MAX_XOVER_REQUEST_SIZE) {
|
|
//
|
|
// Adding this range, plus its leading junk, would push the
|
|
// cumulative request over its maximum allowable size.
|
|
|
|
long reduced_end = req_start + MAX_XOVER_REQUEST_SIZE - 1;
|
|
Assert.Assertion(reduced_end < range_end);
|
|
|
|
if (i == range_start) {
|
|
//
|
|
// This range has no leading junk: it is adjascent to the
|
|
// previously-scheduled range. Request only part of this
|
|
// range, and start accumulating the next batch at the
|
|
// point where we divided this range.
|
|
//
|
|
if (DEBUG)
|
|
System.err.println("NEWSRC: range " +
|
|
range_start + "-" + range_end + " (" +
|
|
(range_end - range_start + 1) + " + " +
|
|
(range_start-i) + ") is large (+" +
|
|
(i - req_start) + " = " +
|
|
(range_end - req_start + 1) +
|
|
");\n cont: " +
|
|
req_start + "-" + reduced_end + " (" +
|
|
(reduced_end - req_start + 1) + ")");
|
|
|
|
store.openNewsgroup(this, req_start, reduced_end);
|
|
i = reduced_end+1;
|
|
req_start = i;
|
|
total_in = 0;
|
|
total_out = 0;
|
|
// (we know that req_start is an unread message.)
|
|
Assert.Assertion(!nl.member(req_start));
|
|
|
|
} else {
|
|
//
|
|
// Adding this range, plus its leading junk, would make the
|
|
// cumulative request size be too large. So ask for the
|
|
// already-accumulated batch (not including this range);
|
|
// and start accumulating the next batch at the beginning of
|
|
// this range (after its leading junk.)
|
|
//
|
|
if (DEBUG)
|
|
System.err.println("NEWSRC: range " +
|
|
range_start + "-" + range_end + " (" +
|
|
(range_end - range_start + 1) + " + " +
|
|
(range_start-i) + ") is large (+" +
|
|
(i - req_start) + " = " +
|
|
(range_end - req_start + 1) +
|
|
");\n load: " +
|
|
req_start + "-" + (i-1) + " (" +
|
|
(i - req_start) + ")");
|
|
|
|
store.openNewsgroup(this, req_start, (i-1));
|
|
Assert.Assertion(i < range_start);
|
|
i = range_start;
|
|
req_start = i;
|
|
total_in = 0;
|
|
total_out = 0;
|
|
// (we know that req_start is an unread message, because
|
|
// it points to the begining of this range.)
|
|
Assert.Assertion(!nl.member(req_start));
|
|
}
|
|
|
|
} else {
|
|
//
|
|
// The range, plus its leading junk, satisfied the size constraint.
|
|
// Now see if it satisfies the allowable-waste constraint.
|
|
|
|
long range_in = (range_end - range_start) + 1;
|
|
long range_out = range_start - i;
|
|
double waste;
|
|
{
|
|
long in = (total_in + range_in);
|
|
long out = (total_out + range_out);
|
|
waste = ((double) out) / ((double) (in + out));
|
|
Assert.Assertion(waste >= 0.0 && waste <= 1.0);
|
|
}
|
|
|
|
if (waste > ALLOWABLE_XOVER_WASTE) {
|
|
//
|
|
// Adding this range, plus its leading junk, would push the
|
|
// cumulative request over its "acceptable junk" ratio.
|
|
// So ask for the already-accumulated batch (not including
|
|
// this range); and start accumulating the next batch at the
|
|
// beginning of this range (after its leading junk.)
|
|
//
|
|
if (DEBUG)
|
|
System.err.println("NEWSRC: range " +
|
|
range_start + "-" + range_end +
|
|
" plus leading waste (" +
|
|
i + "-" + (range_start-1) +
|
|
") would mean " +
|
|
(((int) (waste * 1000)) / 10.0) +
|
|
"% waste; requesting " +
|
|
req_start + "-" + i + " (" +
|
|
(i - range_start + 1) + ")");
|
|
|
|
store.openNewsgroup(this, req_start, i);
|
|
req_start = range_start;
|
|
total_in = 0;
|
|
total_out = 0;
|
|
// (we know that req_start is an unread message.)
|
|
Assert.Assertion(!nl.member(req_start));
|
|
|
|
} else {
|
|
//
|
|
// Adding this range, plus its leading junk, is ok!
|
|
//
|
|
if (DEBUG)
|
|
System.err.println("NEWSRC: range " +
|
|
range_start + "-" + range_end +
|
|
" (" + (range_end - range_start + 1) + " + " +
|
|
(total_out + range_out) + ") added; total " +
|
|
(range_end - req_start + 1) +
|
|
"; wasted " +
|
|
(((int) (waste * 1000)) / 10.0) + "%");
|
|
|
|
total_in += range_in;
|
|
total_out += range_out;
|
|
|
|
i = range_end + 1;
|
|
// If we're not done, then `i' points at a read message
|
|
// (that is, it points at leading junk.)
|
|
Assert.Assertion(i > end || nl.member(i));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Got to end. Is there still stuff in the request buffer?
|
|
if (req_start <= end) {
|
|
|
|
if (DEBUG)
|
|
System.err.println("NEWSRC: done: " +
|
|
req_start + "-" + end + " (" +
|
|
(end - req_start + 1) + ")");
|
|
|
|
store.openNewsgroup(this, req_start, end);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
public void open(int mode) {
|
|
if (DEBUG)
|
|
System.err.println("NF.open " + name);
|
|
ensureLoaded();
|
|
}
|
|
|
|
public void close(boolean doExpunge) throws MessagingException {
|
|
NewsRC rc = store.getNewsRC();
|
|
if (rc != null) {
|
|
try {
|
|
rc.save();
|
|
} catch (IOException e) {
|
|
throw new MessagingException("error saving newsrc file", e);
|
|
}
|
|
}
|
|
fMessages = new Vector();
|
|
}
|
|
|
|
public boolean isOpen() {
|
|
return true; // #### Wrong!
|
|
}
|
|
|
|
InputStream getMessageStream(NewsMessage message, boolean headers_too)
|
|
throws IOException {
|
|
Assert.Assertion(this == message.getFolder());
|
|
return store.getMessageStream(message, headers_too);
|
|
}
|
|
|
|
|
|
/* #### combine this with BerkeleyFolder via MessageBase?
|
|
*/
|
|
/** Assert whether any messages have had their flags changed.
|
|
A child message should call this on its parent when any persistent
|
|
flag value is changed. If a non-null message is provided, and
|
|
flags_dirty is true, then notify any observers that this message
|
|
has changed.
|
|
|
|
@param flags_dirty Whether the flags should currently be considered
|
|
to be dirty.
|
|
|
|
@param message If flags_dirty, the Message (BerkeleyMessage) that
|
|
has become dirty.
|
|
|
|
@param old_flags If message is non-null, the previous value of its
|
|
flags (Message.flags should be the new, dirty value.)
|
|
*/
|
|
void setFlagsDirty(boolean flags_dirty, Message message, long old_flags) {
|
|
|
|
// If the read-ness of a message has changed, flush the new counts down
|
|
// into the newsrc object.
|
|
//
|
|
if (flags_dirty && message != null) {
|
|
long new_flags = ((MessageBase)message).flags;
|
|
long interesting = (MessageBase.FLAG_READ);
|
|
if ((old_flags & interesting) != (new_flags & interesting)) {
|
|
NewsRC rc = store.getNewsRC();
|
|
NewsRCLine nl = rc.getNewsgroup(getFullName());
|
|
int n = ((NewsMessage)message).getStorageFolderIndex();
|
|
if (n < 0)
|
|
; // Eh?
|
|
else if ((new_flags & MessageBase.FLAG_READ) != 0)
|
|
nl.insert(n);
|
|
else
|
|
nl.delete(n);
|
|
}
|
|
}
|
|
|
|
if (flags_dirty && message != null) {
|
|
notifyMessageChangedListeners(MessageChangedEvent.FLAGS_CHANGED,
|
|
message);
|
|
}
|
|
}
|
|
|
|
}
|