001: // The contents of this file are subject to the Mozilla Public License Version
002: // 1.1
003: //(the "License"); you may not use this file except in compliance with the
004: //License. You may obtain a copy of the License at http://www.mozilla.org/MPL/
005: //
006: //Software distributed under the License is distributed on an "AS IS" basis,
007: //WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
008: //for the specific language governing rights and
009: //limitations under the License.
010: //
011: //The Original Code is "The Columba Project"
012: //
013: //The Initial Developers of the Original Code are Frederik Dietz and Timo
014: // Stich.
015: //Portions created by Frederik Dietz and Timo Stich are Copyright (C) 2003.
016: //
017: //All Rights Reserved.
018: package org.columba.mail.gui.table.model;
019:
020: import java.text.Collator;
021: import java.util.Collections;
022: import java.util.Comparator;
023: import java.util.Date;
024: import java.util.Enumeration;
025: import java.util.HashMap;
026: import java.util.Iterator;
027: import java.util.List;
028: import java.util.StringTokenizer;
029: import java.util.logging.Logger;
030:
031: import org.columba.mail.message.ColumbaHeader;
032: import org.columba.mail.message.IColumbaHeader;
033:
034: /**
035: * Threaded model using Message-Id:, In-Reply-To: and References: headers to
036: * create a tree structure of discussions.
037: * <p>
038: * The algorithm works this way:
039: * <ul>
040: * <li>save every header in hashmap, use Message-Id: as key, MessageNode as
041: * value</li>
042: * <li>for each header, check if In-Reply-To:, or References: points to a
043: * matching Message-Id:. If match was found, add header as child</li>
044: * </ul>
045: * <p>
046: *
047: * @author fdietz
048: */
049: public class TableModelThreadedView implements ModelVisitor {
050:
051: /** JDK 1.4+ logging framework logger, used for logging. */
052: private static final Logger LOG = Logger
053: .getLogger("org.columba.mail.gui.table.model");
054:
055: private boolean enabled;
056:
057: private HashMap hashtable;
058:
059: private int idCount = 0;
060:
061: private Collator collator;
062:
063: public TableModelThreadedView() {
064:
065: enabled = false;
066:
067: collator = Collator.getInstance();
068: }
069:
070: public boolean isEnabled() {
071: return enabled;
072: }
073:
074: public void setEnabled(boolean b) {
075: enabled = b;
076: }
077:
078: /**
079: * Parse References: header value and save every found Message-Id: in array.
080: * <p>
081: * TODO (@author tistch): cleanup tokenizer, this could be much faster using
082: * regexp
083: *
084: * @param references
085: * References: header value
086: *
087: * @return array containing Message-Id: header values
088: */
089: public static String[] parseReferences(String references) {
090:
091: StringTokenizer tk = new StringTokenizer(references, ">");
092: String[] list = new String[tk.countTokens()];
093: int i = 0;
094:
095: while (tk.hasMoreTokens()) {
096: String str = (String) tk.nextToken();
097: str = str.trim();
098: str = str + ">";
099: list[i++] = str;
100:
101: }
102:
103: return list;
104: }
105:
106: protected boolean add(MessageNode node, MessageNode rootNode) {
107: IColumbaHeader header = node.getHeader();
108: String references = (String) header.get("References");
109: String inReply = (String) header.get("In-Reply-To");
110:
111: if (inReply != null) {
112: inReply = inReply.trim();
113:
114: if (hashtable.containsKey(inReply)) {
115:
116: MessageNode parent = (MessageNode) hashtable
117: .get(inReply);
118: if (!parent.isNodeAncestor(node))
119: parent.add(node);
120:
121: return true;
122: }
123: } else if (references != null) {
124: references = references.trim();
125:
126: String[] referenceList = parseReferences(references);
127: int count = referenceList.length;
128:
129: if (count >= 1) {
130: // the last element is the direct parent
131: MessageNode parent = (MessageNode) hashtable
132: .get(referenceList[referenceList.length - 1]
133: .trim());
134: if (parent != null) {
135: parent.add(node);
136: return true;
137: }
138: }
139: }
140:
141: return false;
142: }
143:
144: // create tree structure
145: protected void thread(MessageNode rootNode) {
146: idCount = 0;
147:
148: if (rootNode == null) {
149: return;
150: }
151:
152: // save every MessageNode in hashmap for later reference
153: createHashmap(rootNode);
154:
155: // for each element in the message-header-reference or in-reply-to
156: // headerfield: - find a container whose message-id matches and add
157: // message, otherwise create empty container
158: Iterator it = hashtable.keySet().iterator();
159: while (it.hasNext()) {
160: String key = (String) it.next();
161:
162: MessageNode node = (MessageNode) hashtable.get(key);
163: add(node, rootNode);
164: }
165:
166: }
167:
168: private String getMessageID(MessageNode node) {
169: IColumbaHeader header = node.getHeader();
170:
171: String id = (String) header.get("Message-ID");
172:
173: if (id == null) {
174: id = (String) header.get("Message-Id");
175: }
176:
177: // if no Message-Id: available create bogus
178: if (id == null) {
179: id = new String("<bogus-id:" + (idCount++) + ">");
180: header.set("Message-ID", id);
181: }
182:
183: id = id.trim();
184:
185: return id;
186: }
187:
188: /**
189: * Save every MessageNode in HashMap for later reference.
190: * <p>
191: * Message-Id: is key, MessageNode is value
192: *
193: * @param rootNode
194: * root node
195: */
196: private void createHashmap(MessageNode rootNode) {
197: hashtable = new HashMap(rootNode.getChildCount());
198:
199: // save every message-id in hashtable for later reference
200: for (Enumeration enumeration = rootNode.children(); enumeration
201: .hasMoreElements();) {
202: MessageNode node = (MessageNode) enumeration.nextElement();
203: String id = getMessageID(node);
204:
205: hashtable.put(id, node);
206:
207: }
208: }
209:
210: /**
211: *
212: * sort all children after date
213: *
214: * @param node
215: * root MessageNode
216: */
217: protected void sort(int columnNumber, MessageNode node) {
218: for (int i = 0; i < node.getChildCount(); i++) {
219: MessageNode child = (MessageNode) node.getChildAt(i);
220:
221: //if ( ( child.isLeaf() == false ) && ( !child.getParent().equals(
222: // node ) ) )
223: if (!child.isLeaf()) {
224: // has children
225: List v = child.getVector();
226: Collections.sort(v, new MessageHeaderComparator(
227: columnNumber, true));
228:
229: // check if there are messages marked as recent
230: // -> in case underline parent node
231: boolean contains = containsRecentChildren(child);
232: child.setHasRecentChildren(contains);
233: }
234: }
235: }
236:
237: protected boolean containsRecentChildren(MessageNode parent) {
238: for (int i = 0; i < parent.getChildCount(); i++) {
239: MessageNode child = (MessageNode) parent.getChildAt(i);
240:
241: if (((ColumbaHeader) child.getHeader()).getFlags()
242: .getSeen() == false) {
243: // recent found
244: LOG.info("found recent message");
245:
246: return true;
247: } else {
248: containsRecentChildren(child);
249: }
250: }
251:
252: return false;
253: }
254:
255: class MessageHeaderComparator implements Comparator {
256:
257: protected int column;
258:
259: protected boolean ascending;
260:
261: private String columnName;
262:
263: public MessageHeaderComparator(int sortCol, boolean sortAsc) {
264: column = sortCol;
265: ascending = sortAsc;
266:
267: this .columnName = "Date";
268: }
269:
270: public int compare(Object o1, Object o2) {
271:
272: MessageNode node1 = (MessageNode) o1;
273: MessageNode node2 = (MessageNode) o2;
274:
275: IColumbaHeader header1 = node1.getHeader();
276: IColumbaHeader header2 = node2.getHeader();
277:
278: if ((header1 == null) || (header2 == null)) {
279: return 0;
280: }
281:
282: int result = 0;
283:
284: if (columnName.equals("Date")) {
285: Date d1 = (Date) header1.get("columba.date");
286: Date d2 = (Date) header2.get("columba.date");
287:
288: if ((d1 == null) || (d2 == null)) {
289: result = 0;
290: } else {
291: result = d1.compareTo(d2);
292: }
293: } else {
294: Object item1 = header1.get(columnName);
295: Object item2 = header2.get(columnName);
296:
297: if ((item1 != null) && (item2 == null)) {
298: result = 1;
299: } else if ((item1 == null) && (item2 != null)) {
300: result = -1;
301: } else if ((item1 == null) && (item2 == null)) {
302: result = 0;
303: } else if (item1 instanceof String) {
304: result = collator.compare((String) item1,
305: (String) item2);
306: }
307: }
308:
309: if (!ascending) {
310: result = -result;
311: }
312:
313: return result;
314: }
315:
316: public boolean equals(Object obj) {
317: if (obj instanceof MessageHeaderComparator) {
318: MessageHeaderComparator compObj = (MessageHeaderComparator) obj;
319:
320: return (compObj.column == column)
321: && (compObj.ascending == ascending);
322: }
323:
324: return false;
325: }
326: }
327:
328: /**
329: * @see org.columba.mail.gui.table.model.ModelVisitor#visit(org.columba.mail.gui.table.model.TreeTableModelInterface)
330: */
331: public void visit(HeaderTableModel realModel) {
332: if (enabled == false)
333: return;
334:
335: thread(realModel.getRootNode());
336:
337: // go through whole tree and sort the siblings after date
338: sort(realModel.getColumnNumber("Date"), realModel.getRootNode());
339: }
340: }
|