gecko-dev/grendel/view/Threader.java

588 строки
20 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>, 13 Jun 1995.
* Ported from C on 14 Aug 1997.
*/
package grendel.view;
import java.util.Hashtable;
import java.util.Enumeration;
import calypso.util.Assert;
/**
This arranges a set of messages into a thread hierarchy, by references.
The sets of messages are accessed via the IThreadable interface.
@see IThreadable
@see Sorter
*/
class Threader {
private ThreadContainer root_node; // has kids, and no next
private Hashtable id_table; // maps message IDs to ThreadContainers
private int bogus_id_count = 0; // tick of how many dup IDs we've seen
/** Threads the set of messages indicated by threadable_root.
The IThreadable returned is the new first element of the root set.
@param threadable_root The start of the list.
*/
public IThreadable thread(IThreadable threadable_root) {
if (threadable_root == null) return null;
id_table = new Hashtable();
for (Enumeration e = threadable_root.allElements(); e.hasMoreElements();) {
IThreadable t = (IThreadable) e.nextElement();
if (!t.isDummy())
buildContainer(t);
}
root_node = findRootSet();
id_table.clear();
id_table = null;
pruneEmptyContainers(root_node);
// We do this so to avoid flipping the input order each time through.
root_node.reverseChildren();
gatherSubjects();
if (root_node.next != null)
throw new Error("root node has a next?" + root_node);
for (ThreadContainer r = root_node.child; r != null; r = r.next) {
// If this direct child of the root node has no threadable in it,
// manufacture a dummy container to bind its children together.
// Note that these dummies can only ever occur as elements of
// the root set.
if (r.threadable == null)
r.threadable = r.child.threadable.makeDummy();
}
IThreadable result = (root_node.child == null
? null
: root_node.child.threadable);
// Flush the tree structure of each element of the root set down into
// their underlying threadables.
root_node.flush();
root_node = null;
return result;
}
// buildContainer() does three things:
//
// = It walks the tree of threadables, and wraps each in a
// ThreadContainer object.
// = It indexes each ThreadContainer object in the id_table, under
// the message ID of the contained IThreadable.
// = For each of the IThreadable's references, it ensures that there
// is a ThreadContainer in the table (an empty one, if necessary.)
//
private void buildContainer(IThreadable threadable) {
Object id = threadable.messageThreadID();
Assert.Assertion(id != null);
ThreadContainer c = (ThreadContainer) id_table.get (id);
if (c != null) {
// There is already a ThreadContainer in the table for this ID.
// Under normal circumstances, there will be no IThreadable in it
// (since it was a forward reference from a References field.)
//
// If there is already a threadable in it, then that means there
// are two IThreadables with the same ID. Generate a new ID for
// this one, sigh... This ID is only used to cause the two entries
// in the hash table to not stomp each other.
//
if (c.threadable != null) {
id = "<Bogus-id:" + (bogus_id_count++) + ">";
c = null;
} else {
c.threadable = threadable;
}
}
// Create a ThreadContainer for this IThreadable, and index it in
// the hash table.
//
if (c == null) {
c = new ThreadContainer();
c.threadable = threadable;
// c.debug_id = id;
id_table.put (id, c);
}
// Create ThreadContainers for each of the references which don't
// have them. Link each of the referenced messages together in the
// order implied by the references field, unless they are already
// linked.
ThreadContainer parent_ref = null;
{
Object refs[] = threadable.messageThreadReferences();
int L = (refs == null ? 0 : refs.length);
for (int i = 0; i < L; i++) {
Object ref_string = refs[i];
ThreadContainer ref = (ThreadContainer) id_table.get (ref_string);
if (ref == null) {
ref = new ThreadContainer();
// ref.debug_id = ref_string;
id_table.put (ref_string, ref);
}
// If we have references A B C D, make D be a child of C, etc,
// except if they have parents already.
//
if (parent_ref != null && // there is a parent
ref.parent == null && // don't have a parent already
parent_ref != ref && // not a tight loop
!parent_ref.find_child(ref)) { // not a wide loop
// Ok, link it into the parent's child list.
ref.parent = parent_ref;
ref.next = parent_ref.child;
parent_ref.child = ref;
}
parent_ref = ref;
}
}
// At this point `parent_ref' is set to the container of the last element
// in the references field. Make that be the parent of this container,
// unless doing so would introduce a circularity.
//
if (parent_ref != null &&
(parent_ref == c ||
c.find_child (parent_ref)))
parent_ref = null;
if (c.parent != null) {
// If it has a parent already, that's there because we saw this message
// in a references field, and presumed a parent based on the other
// entries in that field. Now that we have the actual message, we can
// be more definitive, so throw away the old parent and use this new one.
// Find this container in the parent's child-list, and unlink it.
//
// Note that this could cause this message to now have no parent, if it
// has no references field, but some message referred to it as the
// non-first element of its references. (Which would have been some
// kind of lie...)
//
ThreadContainer rest, prev;
for (prev = null, rest = c.parent.child;
rest != null;
prev = rest, rest = rest.next) {
if (rest == c)
break;
}
if (rest == null)
throw new Error("didn't find " + c + " in parent " + c.parent);
if (prev == null)
c.parent.child = c.next;
else
prev.next = c.next;
c.next = null;
c.parent = null;
}
// If we have a parent, link c into the parent's child list.
if (parent_ref != null) {
c.parent = parent_ref;
c.next = parent_ref.child;
parent_ref.child = c;
}
}
// Find the root set of the ThreadContainers (and return a root node.)
// A container is in the root set if it has no parents.
//
private ThreadContainer findRootSet() {
ThreadContainer root = new ThreadContainer();
// root.debug_id = "((root))";
for (Enumeration e = id_table.elements(); e.hasMoreElements(); ) {
ThreadContainer c = (ThreadContainer) e.nextElement();
if (c.parent == null) {
if (c.next != null)
throw new Error("c.next is " + c.next.toString());
c.next = root.child;
root.child = c;
}
}
return root;
}
// Walk through the threads and discard any empty container objects.
// After calling this, there will only be any empty container objects
// at depth 0, and those will all have at least two kids.
//
private void pruneEmptyContainers(ThreadContainer parent) {
ThreadContainer container, prev, next;
for (prev = null, container = parent.child, next = container.next;
container != null;
prev = container, container = next,
next = (container == null ? null : container.next)) {
if (container.threadable == null &&
container.child == null) {
// This is an empty container with no kids. Nuke it.
//
// Normally such containers won't occur, but they can show up when
// two messages have References lines that disagree. For example,
// assuming A and B are messages, and 1, 2, and 3 are references for
// messages we haven't seen:
//
// A has refs: 1 2 3
// B has refs: 1 3
//
// There is ambiguity as to whether 3 is a child of 1 or 2. So,
// depending on the processing order, we might end up with either
//
// -- 1
// |-- 2
// |-- 3
// |-- A
// |-- B
// or
// -- 1
// |-- 2 <--- non root childless container
// |-- 3
// |-- A
// |-- B
//
if (prev == null)
parent.child = container.next;
else
prev.next = container.next;
// Set container to prev so that prev keeps its same value
// the next time through the loop.
container = prev;
} else if (container.threadable == null && // expired, and
container.child != null && // has kids, and
(container.parent != null || // not at root, or
container.child.next == null)) { // only one kid
// Expired message with kids. Promote the kids to this level.
// Don't do this if we would be promoting them to the root level,
// unless there is only one kid.
ThreadContainer tail;
ThreadContainer kids = container.child;
// Remove this container from the list, replacing it with `kids'.
if (prev == null)
parent.child = kids;
else
prev.next = kids;
// make each child's parent be this level's parent.
// make the last child's next be this container's next
// (splicing `kids' into the list in place of `container'.)
for (tail = kids; tail.next != null; tail = tail.next)
tail.parent = container.parent;
tail.parent = container.parent;
tail.next = container.next;
// Since we've inserted items in the chain, `next' currently points
// to the item after them (tail.next); reset that so that we process
// the newly promoted items the very next time around.
next = kids;
// Set container to prev so that prev keeps its same value
// the next time through the loop.
container = prev;
} else if (container.child != null) {
// A real message with kids.
// Iterate over its children, and try to strip out the junk.
pruneEmptyContainers(container);
}
}
}
// If any two members of the root set have the same subject, merge them.
// This is so that messages which don't have References headers at all
// still get threaded (to the extent possible, at least.)
//
private void gatherSubjects() {
int count = 0;
for (ThreadContainer c = root_node.child; c != null; c = c.next)
count++;
// Make the hash table large enough to not need to be rehashed.
Hashtable subj_table = new Hashtable((int) (count * 1.2), (float) 0.9);
count = 0;
for (ThreadContainer c = root_node.child; c != null; c = c.next) {
IThreadable threadable = c.threadable;
// If there is no threadable, this is a dummy node in the root set.
// Only root set members may be dummies, and they always have at least
// two kids. Take the first kid as representative of the subject.
if (threadable == null)
threadable = c.child.threadable;
String subj = threadable.simplifiedSubject();
if (subj == null || subj == "")
continue;
ThreadContainer old = (ThreadContainer) subj_table.get(subj);
// Add this container to the table if:
// - There is no container in the table with this subject, or
// - This one is a dummy container and the old one is not: the dummy
// one is more interesting as a root, so put it in the table instead.
// - The container in the table has a "Re:" version of this subject,
// and this container has a non-"Re:" version of this subject.
// The non-re version is the more interesting of the two.
//
if (old == null ||
(c.threadable == null && old.threadable != null) ||
(old.threadable != null && old.threadable.subjectIsReply() &&
c.threadable != null && !c.threadable.subjectIsReply())) {
subj_table.put(subj, c);
count++;
}
}
if (count == 0) // if the table is empty, we're done.
return;
// The subj_table is now populated with one entry for each subject which
// occurs in the root set. Now iterate over the root set, and gather
// together the difference.
//
ThreadContainer prev, c, rest;
for (prev = null, c = root_node.child, rest = c.next;
c != null;
prev = c, c = rest, rest = (rest == null ? null : rest.next)) {
IThreadable threadable = c.threadable;
if (threadable == null) // might be a dummy -- see above
threadable = c.child.threadable;
String subj = threadable.simplifiedSubject();
// Don't thread together all subjectless messages; let them dangle.
if (subj == null || subj == "")
continue;
ThreadContainer old = (ThreadContainer) subj_table.get(subj);
if (old == c) // oops, that's us
continue;
// Ok, so now we have found another container in the root set with
// the same subject. There are a few possibilities:
//
// - If both are dummies, append one's children to the other, and remove
// the now-empty container.
//
// - If one container is a dummy and the other is not, make the non-dummy
// one be a child of the dummy, and a sibling of the other "real"
// messages with the same subject (the dummy's children.)
//
// - If that container is a non-dummy, and that message's subject does
// not begin with "Re:", but *this* message's subject does, then
// make this be a child of the other.
//
// - If that container is a non-dummy, and that message's subject begins
// with "Re:", but *this* message's subject does *not*, then make that
// be a child of this one -- they were misordered. (This happens
// somewhat implicitly, since if there are two messages, one with Re:
// and one without, the one without will be in the hash table,
// regardless of the order in which they were seen.)
//
// - Otherwise, make a new dummy container and make both messages be a
// child of it. This catches the both-are-replies and neither-are-
// replies cases, and makes them be siblings instead of asserting a
// hierarchical relationship which might not be true.
//
// (People who reply to messages without using "Re:" and without using
// a References line will break this slightly. Those people suck.)
//
// (It has occurred to me that taking the date or message number into
// account would be one way of resolving some of the ambiguous cases,
// but that's not altogether straightforward either.)
// Remove the "second" message from the root set.
if (prev == null)
root_node.child = c.next;
else
prev.next = c.next;
c.next = null;
if (old.threadable == null && c.threadable == null) {
// They're both dummies; merge them.
ThreadContainer tail;
for (tail = old.child;
tail != null && tail.next != null;
tail = tail.next)
;
tail.next = c.child;
for (tail = c.child; tail != null; tail = tail.next)
tail.parent = old;
c.child = null;
} else if (old.threadable == null || // old is empty, or
(c.threadable != null &&
c.threadable.subjectIsReply() && // c has Re, and
!old.threadable.subjectIsReply())) { // old does not.
// Make this message be a child of the other.
c.parent = old;
c.next = old.child;
old.child = c;
} else {
// Make the old and new messages be children of a new dummy container.
// We do this by creating a new container object for old->msg and
// transforming the old container into a dummy (by merely emptying it),
// so that the hash table still points to the one that is at depth 0
// instead of depth 1.
ThreadContainer newc = new ThreadContainer();
newc.threadable = old.threadable;
// newc.debug_id = old.debug_id;
newc.child = old.child;
for (ThreadContainer tail = newc.child; tail != null; tail = tail.next)
tail.parent = newc;
old.threadable = null;
old.child = null;
// old.debug_id = null;
c.parent = old;
newc.parent = old;
// old is now a dummy; make it have exactly two kids, c and newc.
old.child = c;
c.next = newc;
}
// we've done a merge, so keep the same `prev' next time around.
c = prev;
}
subj_table.clear();
subj_table = null;
}
}
/*
The ThreadContainer object is used to encapsulate an IThreadable object
(it holds some intermediate state used while threading.) This is a
private class that doesn't escape from this module.
*/
class ThreadContainer {
IThreadable threadable;
ThreadContainer parent;
ThreadContainer child;
ThreadContainer next;
// Object debug_id;
// int id2;
// static int id2_pool = 0;
// ThreadContainer() { id2 = ++id2_pool; }
// public String toString() { return "<cont " +
// id2 + " " + debug_id +
// " [" + threadable + "] >"; }
// public void debug_print(int depth) {
// for (int i = 0; i < depth; i++) System.out.print(" ");
// System.out.println(this);
// if (child != null) child.debug_print(depth+1);
// if (next != null) next.debug_print(depth);
// }
// Copy the ThreadContainer tree structure down into the underlying
// IThreadable objects (that is, make the IThreadable tree look like
// the ThreadContainer tree.)
//
void flush() {
if (parent != null && threadable == null)
// Only the root_node is allowed to not have a threadable.
throw new Error("no threadable in " + this.toString());
parent = null;
if (threadable != null)
threadable.setChild(child == null ? null : child.threadable);
if (child != null) {
child.flush();
child = null;
}
if (threadable != null)
threadable.setNext(next == null ? null : next.threadable);
if (next != null) {
next.flush();
next = null;
}
threadable = null;
}
// Returns true if child is under self's tree. This is used for
// detecting circularities in the references header.
boolean find_child(ThreadContainer target) {
if (child == null)
return false;
else if (child == target)
return true;
else
return child.find_child(target);
}
void reverseChildren() {
if (child != null) {
// nreverse the children (child through child.next.next.next...)
ThreadContainer kid, prev, rest;
for (prev = null, kid = child, rest = kid.next;
kid != null;
prev = kid, kid = rest, rest = (rest == null ? null : rest.next))
kid.next = prev;
child = prev;
// then do it for the kids
for (kid = child; kid != null; kid = kid.next)
kid.reverseChildren();
}
}
}