pjs/grendel/storage/NewsFolder.java

541 строка
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);
}
}
}