/* -*- 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 , 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); } } }