root/trunk/LogicMail/src/org/logicprobe/LogicMail/model/AccountNode.java

Revision 701, 24.5 KB (checked in by octorian, 37 hours ago)

Refactoring to separate AccountNode into Local and Network subclasses

Line 
1/*-
2 * Copyright (c) 2008, Derek Konigsberg
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 *
9 * 1. Redistributions of source code must retain the above copyright
10 *    notice, this list of conditions and the following disclaimer.
11 * 2. Redistributions in binary form must reproduce the above copyright
12 *    notice, this list of conditions and the following disclaimer in the
13 *    documentation and/or other materials provided with the distribution.
14 * 3. Neither the name of the project nor the names of its
15 *    contributors may be used to endorse or promote products derived
16 *    from this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
21 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
22 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
23 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
24 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
26 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
27 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
29 * OF THE POSSIBILITY OF SUCH DAMAGE.
30 */
31package org.logicprobe.LogicMail.model;
32
33import org.logicprobe.LogicMail.mail.AbstractMailStore;
34import org.logicprobe.LogicMail.mail.FolderEvent;
35import org.logicprobe.LogicMail.mail.FolderListener;
36import org.logicprobe.LogicMail.mail.FolderMessagesEvent;
37import org.logicprobe.LogicMail.mail.FolderTreeItem;
38import org.logicprobe.LogicMail.mail.MailStoreListener;
39import org.logicprobe.LogicMail.mail.MessageEvent;
40import org.logicprobe.LogicMail.mail.MessageListener;
41import org.logicprobe.LogicMail.mail.MessageToken;
42import org.logicprobe.LogicMail.message.FolderMessage;
43import org.logicprobe.LogicMail.util.EventListenerList;
44
45import java.util.Enumeration;
46import java.util.Hashtable;
47import java.util.Vector;
48
49/**
50 * Account node for the mail data model.
51 * This node contains only the root <tt>MailboxNode</tt> instance.
52 * Currently the type of mail store backing this node is
53 * determined by the constructor that is called.
54 * Eventually a more elegant approach will need to
55 * be implemented.
56 */
57public abstract class AccountNode implements Node {
58    public final static int STATUS_LOCAL = 0;
59    public final static int STATUS_OFFLINE = 1;
60    public final static int STATUS_ONLINE = 2;
61    private AbstractMailStore mailStore;
62    private MailRootNode parent;
63    private MailboxNode rootMailbox;
64    private Hashtable pathMailboxMap;
65    private Object rootMailboxLock = new Object();
66    private EventListenerList listenerList = new EventListenerList();
67   
68    protected int status;
69   
70    /** Map of folders to messages to fetch for them. */
71    private Hashtable folderMessagesToFetch;
72   
73    /**
74     * Construct a new node for a network account.
75     *
76     * @param accountConfig Account configuration.
77     */
78    protected AccountNode(AbstractMailStore mailStore) {
79        this.rootMailbox = null;
80        this.pathMailboxMap = new Hashtable();
81        this.folderMessagesToFetch = new Hashtable();
82
83        this.mailStore = mailStore;
84
85        addMailStoreListeners();
86    }
87
88    private void addMailStoreListeners() {
89        this.mailStore.addMailStoreListener(new MailStoreListener() {
90            public void folderTreeUpdated(FolderEvent e) {
91                mailStore_FolderTreeUpdated(e);
92            }
93        });
94
95        this.mailStore.addFolderListener(new FolderListener() {
96            public void folderStatusChanged(FolderEvent e) {
97                mailStore_FolderStatusChanged(e);
98            }
99
100            public void folderMessagesAvailable(FolderMessagesEvent e) {
101                mailStore_FolderMessagesAvailable(e);
102            }
103
104            public void folderExpunged(FolderEvent e) {
105                mailStore_FolderExpunged(e);
106            }
107        });
108
109        this.mailStore.addMessageListener(new MessageListener() {
110            public void messageAvailable(MessageEvent e) {
111                mailStore_messageAvailable(e);
112            }
113
114            public void messageFlagsChanged(MessageEvent e) {
115                mailStore_messageFlagsChanged(e);
116            }
117
118            public void messageDeleted(MessageEvent e) {
119                mailStore_messageDeleted(e);
120            }
121
122            public void messageUndeleted(MessageEvent e) {
123                mailStore_messageUndeleted(e);
124            }
125        });
126    }
127
128    public void accept(NodeVisitor visitor) {
129        visitor.visit(this);
130    }
131
132    /**
133     * Sets the root node which is the parent of this account.
134     *
135     * @param parent The root node.
136     */
137    void setParent(MailRootNode parent) {
138        this.parent = parent;
139    }
140
141    /**
142     * Gets the root node which is the parent of this account.
143     *
144     * @return The root node.
145     */
146    public MailRootNode getParent() {
147        return this.parent;
148    }
149
150    /**
151     * Get the top-level mailbox contained within this account.
152     * This mailbox typically exists only for the purpose of
153     * containing other mailboxes, and is not normally shown
154     * to the user.
155     *
156     * @return Root mailbox node.
157     */
158    public MailboxNode getRootMailbox() {
159        synchronized (rootMailboxLock) {
160            return this.rootMailbox;
161        }
162    }
163
164    /**
165     * Gets the mail store associated with this account.
166     *
167     * @return The mail store.
168     */
169    AbstractMailStore getMailStore() {
170        return this.mailStore;
171    }
172
173    /**
174     * Sets the status of this account.
175     *
176     * @param status The status.
177     */
178    void setStatus(int status) {
179        if (this.status != status) {
180            this.status = status;
181
182            fireAccountStatusChanged(AccountNodeEvent.TYPE_CONNECTION);
183        }
184    }
185
186    /**
187     * Gets the status of this account.
188     *
189     * @return The status.
190     */
191    public int getStatus() {
192        return this.status;
193    }
194
195    /**
196     * Gets whether this account supports folders.
197     * If folders are not supported, then this account will automatically
198     * present a single "INBOX" folder.  However, no other folder-related
199     * operations will have any relevance.
200     *
201     * @return True if supported, false otherwise.
202     */
203    public boolean hasFolders() {
204        return this.mailStore.hasFolders();
205    }
206
207    /**
208     * Gets whether this account supports undelete.
209     *
210     * @return True if supported, false otherwise.
211     */
212    public boolean hasUndelete() {
213        return this.mailStore.hasUndelete();
214    }
215
216    /**
217     * Gets whether this account supports expunging deleted messages.
218     *
219     * @return True if supported, false otherwise.
220     */
221    public boolean hasExpunge() {
222        return this.mailStore.hasExpunge();
223    }
224
225    /**
226     * Called to trigger a refresh of the mailboxes under
227     * this account.  Completion is signaled by an
228     * AccountStatusChanged event.
229     */
230    public void refreshMailboxes() {
231        if (mailStore.hasFolders()) {
232            mailStore.requestFolderTree();
233        }
234    }
235
236    /**
237     * Called to trigger a refresh of message count status
238     * for mailboxes under this account.  Completion is
239     * signaled by MailboxStatusChanged events on the
240     * updated mailboxes.
241     */
242    public void refreshMailboxStatus() {
243        int size = pathMailboxMap.size();
244        FolderTreeItem[] folders = new FolderTreeItem[size];
245        Enumeration e = pathMailboxMap.keys();
246
247        for (int i = 0; i < size; i++) {
248            folders[i] = ((MailboxNode) pathMailboxMap.get(e.nextElement())).getFolderTreeItem();
249        }
250
251        mailStore.requestFolderStatus(folders);
252    }
253
254    /**
255     * Handles folder tree updates.
256     *
257     * @param e Event data.
258     */
259    private void mailStore_FolderTreeUpdated(FolderEvent e) {
260        FolderTreeItem rootFolder = e.getFolder();
261
262        synchronized (rootMailboxLock) {
263            Hashtable remainingMailboxMap = new Hashtable();
264
265            if (rootMailbox != null) {
266                // Disassemble the model tree into a flat collection of nodes
267                Vector flatMailboxes = new Vector();
268                populateFlatMailboxes(flatMailboxes, rootMailbox);
269                rootMailbox = null;
270
271                // Prune the collection to only include nodes that are still valid,
272                // and make them reference the new FolderTreeItem objects.
273                Hashtable folderPathMap = new Hashtable();
274                populateFolderPathMap(folderPathMap, rootFolder);
275
276                int size = flatMailboxes.size();
277
278                for (int i = 0; i < size; i++) {
279                    MailboxNode mailboxNode = (MailboxNode) flatMailboxes.elementAt(i);
280                    String path = mailboxNode.getFolderTreeItem().getPath();
281
282                    if (folderPathMap.containsKey(path)) {
283                        mailboxNode.setFolderTreeItem((FolderTreeItem) folderPathMap.get(path));
284                        remainingMailboxMap.put(path, mailboxNode);
285                    }
286                }
287            }
288
289            // Build a new tree from the FolderTreeItem, using the collected
290            // nodes where possible, and new nodes when necessary.
291            this.pathMailboxMap.clear();
292            this.rootMailbox = new MailboxNode(rootFolder, false, -1);
293            populateMailboxNodes(rootFolder, rootMailbox, remainingMailboxMap);
294        }
295
296        save();
297        fireAccountStatusChanged(AccountNodeEvent.TYPE_MAILBOX_TREE);
298    }
299
300    private void populateFlatMailboxes(Vector flatMailboxes,
301        MailboxNode currentMailbox) {
302        flatMailboxes.addElement(currentMailbox);
303
304        MailboxNode[] childNodes = currentMailbox.getMailboxes();
305
306        for (int i = 0; i < childNodes.length; i++) {
307            populateFlatMailboxes(flatMailboxes, childNodes[i]);
308        }
309
310        currentMailbox.clearMailboxes();
311    }
312
313    private void populateFolderPathMap(Hashtable folderPathMap,
314        FolderTreeItem folderTreeItem) {
315        if (folderTreeItem != null) {
316            folderPathMap.put(folderTreeItem.getPath(), folderTreeItem);
317
318            if (folderTreeItem.hasChildren()) {
319                FolderTreeItem[] children = folderTreeItem.children();
320
321                for (int i = 0; i < children.length; i++) {
322                    populateFolderPathMap(folderPathMap, children[i]);
323                }
324            }
325        }
326    }
327
328    private void populateMailboxNodes(FolderTreeItem folderTreeItem,
329        MailboxNode currentMailbox, Hashtable remainingMailboxMap) {
330        pathMailboxMap.put(folderTreeItem.getPath(), currentMailbox);
331
332        if (folderTreeItem.hasChildren()) {
333            FolderTreeItem[] folderTreeItemChildren = folderTreeItem.children();
334
335            for (int i = 0; i < folderTreeItemChildren.length; i++) {
336                MailboxNode childMailbox;
337
338                if (remainingMailboxMap.containsKey(
339                            folderTreeItemChildren[i].getPath())) {
340                    childMailbox = (MailboxNode) remainingMailboxMap.get(folderTreeItemChildren[i].getPath());
341                } else {
342                        int mailboxType = getMailboxType(folderTreeItemChildren[i]);
343                        if(mailboxType == MailboxNode.TYPE_OUTBOX) {
344                            childMailbox = new OutboxMailboxNode(folderTreeItemChildren[i]);
345                        }
346                        else {
347                            childMailbox = new MailboxNode(folderTreeItemChildren[i],
348                                        folderTreeItemChildren[i].isAppendable(),
349                                        mailboxType);
350                        }
351                    childMailbox.setParentAccount(this);
352                }
353
354                populateMailboxNodes(folderTreeItemChildren[i], childMailbox,
355                    remainingMailboxMap);
356                currentMailbox.addMailbox(childMailbox);
357            }
358        }
359    }
360
361    /**
362     * Handles folder status changes.
363     *
364     * @param e Event data.
365     */
366    private void mailStore_FolderStatusChanged(FolderEvent e) {
367        updateMailboxStatus(e.getFolder());
368    }
369
370    /**
371     * Recursively update mailbox status.
372     *
373     * @param currentFolder Folder item to start from.
374     */
375    private void updateMailboxStatus(FolderTreeItem currentFolder) {
376        MailboxNode mailboxNode = (MailboxNode) pathMailboxMap.get(currentFolder.getPath());
377
378        if (mailboxNode != null) {
379            FolderTreeItem mailboxFolder = mailboxNode.getFolderTreeItem();
380            mailboxFolder.setMsgCount(currentFolder.getMsgCount());
381            mailboxFolder.setUnseenCount(currentFolder.getUnseenCount());
382            mailboxNode.updateUnseenFolderTreeItem();
383            mailboxNode.fireMailboxStatusChanged(MailboxNodeEvent.TYPE_STATUS, null);
384        }
385
386        if (currentFolder.hasChildren()) {
387            FolderTreeItem[] children = currentFolder.children();
388
389            for (int i = 0; i < children.length; i++) {
390                updateMailboxStatus(children[i]);
391            }
392        }
393    }
394
395    /**
396     * Handles folder messages becoming available.
397     *
398     * @param e Event data.
399     */
400    private void mailStore_FolderMessagesAvailable(FolderMessagesEvent e) {
401        if (e.getMessages() != null) {
402            // Find the MailboxNode that this event applies to.
403            // If none apply, then shortcut out of here.
404            if (!pathMailboxMap.containsKey(e.getFolder().getPath())) {
405                return;
406            }
407
408            MailboxNode mailboxNode = (MailboxNode) pathMailboxMap.get(e.getFolder().getPath());
409            FolderMessage[] folderMessages = e.getMessages();
410
411            if(e.isFlagsOnly()) {
412                // Only flags have been retrieved, so the existing messages need to
413                // be checked and additional actions requested accordingly.
414                    for (int i = 0; i < folderMessages.length; i++) {
415                        if(!mailboxNode.updateMessageFlags(folderMessages[i].getMessageToken(), folderMessages[i].getFlags())) {
416                        // Message does not currently exist in the mailbox, and
417                        // is not in the process of being loaded from cache.
418                        synchronized(folderMessagesToFetch) {
419                            Vector messagesToFetch = (Vector)folderMessagesToFetch.get(e.getFolder());
420                            if(messagesToFetch == null) {
421                                messagesToFetch = new Vector();
422                                folderMessagesToFetch.put(e.getFolder(), messagesToFetch);
423                            }
424                            messagesToFetch.addElement(folderMessages[i].getMessageToken());
425                        }
426                        }
427                    }
428            }
429            else {
430                    // Determine what MessageNodes need to be created, and add them.
431                    Vector addedMessages = new Vector();
432       
433                    for (int i = 0; i < folderMessages.length; i++) {
434                        MessageNode messageNode = new MessageNode(folderMessages[i]);
435                        messageNode.setExistsOnServer(true);
436                        addedMessages.addElement(messageNode);
437                    }
438       
439                    MessageNode[] addedMessagesArray = new MessageNode[addedMessages.size()];
440                    addedMessages.copyInto(addedMessagesArray);
441                    mailboxNode.addMessages(addedMessagesArray);
442            }
443        }
444        else {
445            synchronized(folderMessagesToFetch) {
446                FolderTreeItem fetchFolder = e.getFolder();
447               
448                // Clean out all messages that couldn't be verified against the server
449                MailboxNode mailboxNode = (MailboxNode) pathMailboxMap.get(fetchFolder.getPath());
450                mailboxNode.removeMessagesNotOnServer();
451               
452                // Queue a fetch for messages that do exist
453                Vector messagesToFetch = (Vector)folderMessagesToFetch.remove(fetchFolder);
454                if(messagesToFetch != null) {
455                    MessageToken[] fetchArray = new MessageToken[messagesToFetch.size()];
456                    messagesToFetch.copyInto(fetchArray);
457                    mailStore.requestFolderMessagesSet(e.getFolder(), fetchArray);
458                }
459            }
460        }
461    }
462
463    /**
464     * Handles a folder being expunged.
465     *
466     * @param e Event data.
467     */
468    private void mailStore_FolderExpunged(FolderEvent e) {
469        FolderTreeItem fetchFolder = e.getFolder();
470        MailboxNode mailboxNode = (MailboxNode) pathMailboxMap.get(fetchFolder.getPath());
471        mailboxNode.handleExpungeNotification();
472    }
473
474    /**
475     * Handles a message being loaded.
476     *
477     * @param e Event data.
478     */
479    private void mailStore_messageAvailable(MessageEvent e) {
480        MessageNode messageNode = findMessageForToken(e.getMessageToken());
481
482        if (messageNode != null) {
483            // Set the SEEN bit and unset the RECENT bit
484            int flags = messageNode.getFlags();
485            flags |= MessageNode.Flag.SEEN;
486            flags &= ~MessageNode.Flag.RECENT;
487
488            messageNode.setFlags(flags);
489
490                switch(e.getType()) {
491                case MessageEvent.TYPE_FULLY_LOADED:
492                messageNode.setMessageStructure(e.getMessageStructure());
493                messageNode.setMessageSource(e.getMessageSource());
494                messageNode.putMessageContent(e.getMessageContent());
495                messageNode.commitMessage();
496                break;
497                case MessageEvent.TYPE_CONTENT_LOADED:
498                messageNode.putMessageContent(e.getMessageContent());
499                messageNode.commitMessage();
500                break;
501                }
502        }
503    }
504
505    /**
506     * Handles a message flags changing.
507     *
508     * @param e Event data.
509     */
510    private void mailStore_messageFlagsChanged(MessageEvent e) {
511        MessageNode messageNode = findMessageForToken(e.getMessageToken());
512
513        if (messageNode != null) {
514                messageNode.setFlags(MessageNode.convertMessageFlags(e.getMessageFlags()));
515            messageNode.fireMessageStatusChanged(MessageNodeEvent.TYPE_FLAGS);
516        }
517    }
518
519    /**
520     * Handles a message being deleted.
521     *
522     * @param e Event data.
523     */
524    private void mailStore_messageDeleted(MessageEvent e) {
525        MessageNode messageNode = findMessageForToken(e.getMessageToken());
526
527        if (messageNode != null) {
528                messageNode.setFlags(MessageNode.convertMessageFlags(e.getMessageFlags()));
529            messageNode.fireMessageStatusChanged(MessageNodeEvent.TYPE_FLAGS);
530        }
531    }
532
533    /**
534     * Handles a message being undeleted.
535     *
536     * @param e Event data.
537     */
538    private void mailStore_messageUndeleted(MessageEvent e) {
539        MessageNode messageNode = findMessageForToken(e.getMessageToken());
540
541        if (messageNode != null) {
542                messageNode.setFlags(MessageNode.convertMessageFlags(e.getMessageFlags()));
543            messageNode.fireMessageStatusChanged(MessageNodeEvent.TYPE_FLAGS);
544        }
545    }
546
547    /**
548     * Finds the message node matching a particular token.
549     *
550     * @param messageToken
551     * @return Message node, or null if none was found.
552     */
553    private MessageNode findMessageForToken(MessageToken messageToken) {
554        MailboxNode mailboxNode = null;
555        Enumeration e = pathMailboxMap.elements();
556        while(e.hasMoreElements()) {
557                MailboxNode currentMailbox = (MailboxNode)e.nextElement();
558                if(messageToken.containedWithin(currentMailbox.getFolderTreeItem())) {
559                        mailboxNode = currentMailbox;
560                        break;
561                }
562        }
563       
564        if (mailboxNode != null) {
565            // Change this to use the a real tag object once implemented
566            return mailboxNode.getMessageByToken(messageToken);
567        } else {
568            return null;
569        }
570    }
571
572    /**
573     * Attempts to determine the folder type based on its name,
574     * and any configuration options.
575     * <p>
576     * This approach is only necessary to for local folders and general
577     * defaults.  Explicitly configured special folders have their type set
578     * later in the account loading process.
579     * </p>
580     * @param folderTreeItem Source folder tree item.
581     * @return Mailbox type
582     */
583    protected int getMailboxType(FolderTreeItem folderTreeItem) {
584        int mailboxType;
585        if (folderTreeItem.getPath().equalsIgnoreCase("INBOX")) {
586                mailboxType = MailboxNode.TYPE_INBOX;
587        }
588        else {
589            mailboxType = MailboxNode.TYPE_NORMAL;
590        }
591        return mailboxType;
592    }
593
594    /**
595    * Adds a <tt>AccountNodeListener</tt> to the account node.
596    *
597    * @param l The <tt>AccountNodeListener</tt> to be added.
598    */
599    public void addAccountNodeListener(AccountNodeListener l) {
600        listenerList.add(AccountNodeListener.class, l);
601    }
602
603    /**
604     * Removes a <tt>AccountNodeListener</tt> from the account node.
605     *
606     * @param l The <tt>AccountNodeListener</tt> to be removed.
607     */
608    public void removeAccountNodeListener(AccountNodeListener l) {
609        listenerList.remove(AccountNodeListener.class, l);
610    }
611
612    /**
613     * Returns an array of all <tt>AccountNodeListener</tt>s
614     * that have been added to this account node.
615     *
616     * @return All the <tt>AccountNodeListener</tt>s that have been added,
617     * or an empty array if no listeners have been added.
618     */
619    public AccountNodeListener[] getAccountNodeListeners() {
620        return (AccountNodeListener[]) listenerList.getListeners(AccountNodeListener.class);
621    }
622
623    /**
624     * Notifies all registered <tt>AccountNodeListener</tt>s that
625     * the account status has changed.
626     *
627     * @param type Event type.
628     */
629    protected void fireAccountStatusChanged(int type) {
630        Object[] listeners = listenerList.getListeners(AccountNodeListener.class);
631        AccountNodeEvent e = null;
632
633        for (int i = 0; i < listeners.length; i++) {
634            if (e == null) {
635                e = new AccountNodeEvent(this, type);
636            }
637
638            ((AccountNodeListener) listeners[i]).accountStatusChanged(e);
639        }
640    }
641
642    /**
643     * Saves the mailbox tree to persistent storage.
644     */
645    abstract void save();
646
647    /**
648     * Loads the mailbox tree from persistent storage.
649     */
650    abstract void load();
651
652    /**
653     * Clear any persistent data associated with this account node.
654     *
655     * <p>
656     * When this account node removed from the model tree because the
657     * underlying account has been deleted, this method needs to be called to
658     * ensure that persistent data does not linger on the device.
659     * </p>
660     */
661    protected void removeSavedData() {
662        // Default empty implementation
663    }
664
665    /**
666     * Sets the top-level mailbox contained within this account.
667     * This method should only be called by subclasses when loading saved
668     * account data.
669     */
670    protected void setRootMailbox(MailboxNode mailboxNode) {
671        synchronized (rootMailboxLock) {
672            this.rootMailbox = mailboxNode;
673            prepareDeserializedMailboxNode(rootMailbox);
674        }
675    }
676   
677    /**
678     * Traverses the deserialized mailbox nodes, populates any necessary
679     * data structures in the account node, and sets the mailbox parent
680     * account references.
681     *
682     * @param mailboxNode The mailbox node.
683     */
684    private void prepareDeserializedMailboxNode(MailboxNode mailboxNode) {
685        mailboxNode.setParentAccount(this);
686
687        FolderTreeItem item = mailboxNode.getFolderTreeItem();
688
689        if ((item != null) && (item.getPath().length() > 0)) {
690            this.pathMailboxMap.put(item.getPath(), mailboxNode);
691        }
692
693        MailboxNode[] children = mailboxNode.getMailboxes();
694
695        for (int i = 0; i < children.length; i++) {
696            prepareDeserializedMailboxNode(children[i]);
697        }
698    }
699}
Note: See TracBrowser for help on using the browser.