View Javadoc

1   /****************************************************************
2    * Licensed to the Apache Software Foundation (ASF) under one   *
3    * or more contributor license agreements.  See the NOTICE file *
4    * distributed with this work for additional information        *
5    * regarding copyright ownership.  The ASF licenses this file   *
6    * to you under the Apache License, Version 2.0 (the            *
7    * "License"); you may not use this file except in compliance   *
8    * with the License.  You may obtain a copy of the License at   *
9    *                                                              *
10   *   http://www.apache.org/licenses/LICENSE-2.0                 *
11   *                                                              *
12   * Unless required by applicable law or agreed to in writing,   *
13   * software distributed under the License is distributed on an  *
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
15   * KIND, either express or implied.  See the License for the    *
16   * specific language governing permissions and limitations      *
17   * under the License.                                           *
18   ****************************************************************/
19  
20  package org.apache.james.mailrepository.javamail;
21  
22  import java.io.IOException;
23  import java.io.OutputStream;
24  import java.util.Arrays;
25  import java.util.Collection;
26  import java.util.HashMap;
27  import java.util.HashSet;
28  import java.util.Iterator;
29  import java.util.Map;
30  import java.util.Set;
31  import java.util.SortedMap;
32  import java.util.TreeMap;
33  
34  import javax.mail.Flags;
35  import javax.mail.Folder;
36  import javax.mail.Message;
37  import javax.mail.MessagingException;
38  import javax.mail.Flags.Flag;
39  import javax.mail.internet.MimeMessage;
40  
41  import org.apache.james.core.MailImpl;
42  import org.apache.james.util.stream.CRLFOutputStream;
43  import org.apache.mailet.Mail;
44  
45  /**
46   * MailRepository implementation to store mail in a Javamail store. <br>
47   * should work with every JavamailStore implementation that has deterministic
48   * message content. (checksum save). This implementation should be considered as
49   * EXPERIMENTAL.
50   * 
51   * TODO examine for thread-safety
52   */
53  public class HashJavamailStoreMailRepository extends
54          AbstractJavamailStoreMailRepository {
55  
56      /**
57       * tridirectional map of messages key, hash and number saved in internaly
58       * used class MsgObj
59       */
60      protected KeyToMsgMap keyToMsgMap = null;
61  
62      
63      private boolean getMessageCountOnClosed =true;
64      
65      
66      protected int getMessageCount() throws MessagingException {
67          try {
68              getFolderGateKeeper().use();
69              int n=-1;
70              if (getMessageCountOnClosed) {
71                  n=getFolderGateKeeper().getFolder().getMessageCount();
72                  if (n==-1) {
73                      getMessageCountOnClosed=false;
74                  }
75              }
76              if (!getMessageCountOnClosed) {
77                  n=getFolderGateKeeper().getOpenFolder().getMessageCount();
78              }
79              return n;
80          } finally {
81              getFolderGateKeeper().free();
82          }
83      }
84      
85      /**
86       * Stores a message by Javamails appendMessages method. Tries to guess
87       * resulting messagenumber and saves result in keyToMsgMap. If Folder
88       * supports getMessageCount on closed folder, this could be quite efficient
89       * 
90       * @see org.apache.james.services.MailRepository#store(Mail)
91       */
92      public synchronized void store(Mail mc) throws MessagingException {
93  
94          final String key = mc.getName();
95          boolean wasLocked = true;
96          log.debug("Store: (hash) " + key);
97          if (!mc.getMessage().isSet(Flag.RECENT)) {
98              log.debug("Message didn't have RECENT flag");
99              mc.getMessage().setFlag(Flag.RECENT,true);
100         }
101         // because we use/free/use we need to know the state in finally
102         boolean use=false;
103         try {
104             
105             // Shouldn't we care when another Thread has locked this key and
106             // stop here?
107             synchronized (this) {
108                 wasLocked = getLock().isLocked(key);
109                 if (!wasLocked) {
110                     // If it wasn't locked, we want a lock during the store
111                     lock(key);
112                 }
113             }
114             
115             
116             // Yes, appendMessages works on a closed inbox. But we need
117             // getMessageCount() and that is allowed
118             // to be -1 on a closed Folder when counting messages is expensive
119 
120             int countBefore = getMessageCount();
121             
122             getFolderGateKeeper().use();
123             use=true;
124             // insert or update, don't call remove(key) because of locking
125             if (getKeyToMsgMap().contains(key)) {
126                 log.debug("store is a update");
127                 Message mm = getMessageFromInbox(key);
128                 if (mm != null) {
129                     countBefore--;
130                     mm.setFlag(Flags.Flag.DELETED, true);
131                     mc.getMessage().setFlag(Flags.Flag.RECENT, false);
132                 }
133                 getKeyToMsgMap().removeByKey(key, true);
134             }
135             getFolderGateKeeper().getFolder().appendMessages(new Message[] { mc.getMessage() });
136             use=false;
137             getFolderGateKeeper().free();
138 
139             // Try to guess resulting message number
140             int no = -1;
141             int count=getMessageCount();
142             if (count - countBefore == 1) {
143                 no = count;
144                 log.debug("Assigned message number "+ count);
145             } else {
146                 log.debug("count - countBefore = "+ (count - countBefore ));
147             }
148 
149             getKeyToMsgMap().put(mc.getMessage(), mc.getName(), no);
150         } catch (MessagingException e) {
151             log.error("Exception in HashJavamailStore: ", e);
152             throw e;
153         } finally {
154             if (!wasLocked) {
155                 // If it wasn't locked, we need to unlock now
156                 unlock(key);
157                 synchronized (this) {
158                     notify();
159                 }
160             }
161             if (use) {
162                 getFolderGateKeeper().free();
163             }
164             log.debug("closed.");
165         }
166         log.debug("store finished");
167     }
168 
169     /**
170      * calls rehash and uses stored keys in KeyToMsgMap
171      * 
172      * @see org.apache.james.services.MailRepository#list()
173      */
174     public Iterator list() throws MessagingException {
175         try {
176             getFolderGateKeeper().use();
177             log.debug("list()");
178             rehash(null);
179             final String[] keys = getKeyToMsgMap().getKeys();
180             final Iterator it = Arrays.asList(keys).iterator();
181             return it;
182         } catch (MessagingException e) {
183             throw e;
184         } finally {
185             getFolderGateKeeper().free();
186         }
187 
188     }
189 
190     /**
191      * uses getMessageFromInbox, returns null if not found
192      * 
193      * @see org.apache.james.services.MailRepository#retrieve(String)
194      */
195     public Mail retrieve(String key) throws MessagingException {
196         log.debug("retrieve: " + key);
197         Mail m = null;
198         try {
199             getFolderGateKeeper().use();
200             MimeMessage mm = getMessageFromInbox(key);
201             if (mm != null) {
202                 m = new MailImpl();
203                 m.setMessage(mm);
204                 m.setName(key);
205             } else {
206                 log.debug("could not retrieve a MimeMessage from folder");
207             }
208 
209         } catch (MessagingException e) {
210             throw e;
211         } finally {
212            getFolderGateKeeper().free();
213         }
214         return m;
215     }
216 
217     /**
218      * Removes a message identified by key. uses getMessageFromInbox and does a
219      * setFlag(Flags.Flag.DELETED, true); on message. removes message from
220      * KeyToMsgMap. Messagenumbers are recalculated for next guesses.
221      * 
222      * @see org.apache.james.services.MailRepository#remove(String)
223      */
224     public synchronized void  remove(String key) throws MessagingException {
225         log.debug("HashJavamailStore remove key:" + key);
226         if (lock(key)) {
227             try {
228                 getFolderGateKeeper().use();
229                 Message mm = getMessageFromInbox(key);
230                 if (mm != null) {
231                     // will be deleted on expunge
232                     mm.setFlag(Flags.Flag.DELETED, true);
233                 }
234                 getKeyToMsgMap().removeByKey(key, true);
235             } catch (MessagingException e) {
236                 throw e;
237             } finally {
238                 unlock(key);
239                 getFolderGateKeeper().free();
240             }
241         } else {
242             log.debug("could not optain lock");
243             throw new MessagingException("could not optain lock for remove");
244         }
245     }
246 
247     /**
248      * Calls getMessages(); on Folder and rehashes messages an renews message
249      * numbers calls retainAllListedAndAddedByKeys on KeyToMsgMap to remove keys
250      * not found in Folder
251      * 
252      * @param filterkey
253      *            key of message that should be returned, can be null
254      * @return a message if found by filterkey
255      * @throws MessagingException
256      */
257     protected MimeMessage rehash(String filterkey) throws MessagingException {
258         if (DEEP_DEBUG)
259             log.debug("doing rehash");
260         String[] keysBefore = getKeyToMsgMap().getKeys();
261         MimeMessage mm = null;
262         Message[] msgs = getFolderGateKeeper().getOpenFolder().getMessages();
263         String[] keys = new String[msgs.length];
264         for (int i = 0; i < msgs.length; i++) {
265             Message message = msgs[i];
266             MsgObj mo = getKeyToMsgMap()
267                     .put((MimeMessage) message, null, i + 1);
268             keys[i] = mo.key;
269             if (DEEP_DEBUG)
270                 log.debug("No " + mo.no + " key:" + mo.key);
271             if (mo.key.equals(filterkey)) {
272                 if (DEEP_DEBUG)
273                     log.debug("Found message!");
274                 mm = (MimeMessage) message;
275             }
276         }
277         getKeyToMsgMap().retainAllListedAndAddedByKeys(keysBefore, keys);
278         return mm;
279     }
280 
281     /**
282      * Fetches a message from inbox. Fast fails if key is unknown in
283      * KeyToMsgMap. Tries to get message at last known position, if that was not
284      * successfull calls rehash
285      * 
286      * @param key
287      *            message key
288      * @return message if found, otherwise null
289      * @throws MessagingException
290      */
291     protected MimeMessage getMessageFromInbox(String key)
292             throws MessagingException {
293         MsgObj mo = getKeyToMsgMap().getByKey(key);
294         log.debug("getMessageFromInbox: Looking for hash "+mo.hash);
295         if (mo == null) {
296             log.debug("Key not found");
297             return null;
298         }
299         MimeMessage mm = null;
300         if (cacheMessages && mo.message != null) {
301             // not used at the moment
302             mm = mo.message;
303         } else {
304             try {
305                 getFolderGateKeeper().use();
306                 Object hash = null;
307                 if (mo.no >= 0) {
308                     try {
309                         mm = (MimeMessage) getFolderGateKeeper().getOpenFolder()
310                                 .getMessage(mo.no);
311                         hash = calcMessageHash(mm);
312                         if (!hash.equals(mo.hash)) {
313                             log
314                                     .debug("Message at guessed position does not match "
315                                             + mo.no);
316                             mm = null;
317                         }
318                     } catch (IndexOutOfBoundsException e) {
319                         log.debug("no Message found at guessed position "
320                                 + mo.no);
321                     }
322                 } else {
323                     log.debug("cannot guess message number");
324                 }
325                 if (mm == null) {
326                     mm = rehash(mo.key);
327                     if (mm == null)
328                         log.debug("rehashing was fruitless");
329                 }
330             } finally {
331                 getFolderGateKeeper().free();
332             }
333         }
334         return mm;
335     }
336 
337     /**
338      * lazy loads KeyToMsgMap
339      * 
340      * @return keyToMsgMap the KeyToMsgMap
341      */
342     protected KeyToMsgMap getKeyToMsgMap() {
343         if (keyToMsgMap == null) {
344             keyToMsgMap = new KeyToMsgMap();
345         }
346         return keyToMsgMap;
347     }
348     
349     public static final class HasherOutputStream extends OutputStream {
350         int hashCode = 1;
351         
352         public void write(int b) throws IOException {
353             hashCode = 41 * hashCode + b;
354         }
355 
356         public int getHash() {
357             return hashCode;
358         }
359     }
360 
361 
362 
363     protected class KeyToMsgMap {
364         protected SortedMap noToMsgObj;
365 
366         protected Map keyToMsgObj;
367 
368         protected Map hashToMsgObj;
369 
370         protected KeyToMsgMap() {
371             noToMsgObj = new TreeMap();
372             keyToMsgObj = new HashMap();
373             hashToMsgObj = new HashMap();
374         }
375 
376         /**
377          * Return true if a MsgObj is stored with the given key
378          * 
379          * @param key the key 
380          * @return true if a MsgObj is stored with the given key
381          */
382         public synchronized boolean contains(String key) {
383             return keyToMsgObj.containsKey(key);
384         }
385 
386         /**
387          * Cleans up database after rehash. Only keys listed by rehash or added
388          * in the meantime are retained
389          * 
390          * @param keysBefore
391          *            keys that have exist before rehash was called
392          * @param listed
393          *            keys that have listed by rehash
394          */
395         public synchronized void retainAllListedAndAddedByKeys(
396                 String[] keysBefore, String[] listed) {
397             if (DEEP_DEBUG)
398                 log.debug("stat before retain: " + getStat());
399             Set added = new HashSet(keyToMsgObj.keySet());
400             added.removeAll(Arrays.asList(keysBefore));
401 
402             Set retain = new HashSet(Arrays.asList(listed));
403             retain.addAll(added);
404 
405             Collection remove = new HashSet(keyToMsgObj.keySet());
406             remove.removeAll(retain);
407             // huh, are we turning in circles? :-)
408 
409             for (Iterator iter = remove.iterator(); iter.hasNext();) {
410                 removeByKey((String) iter.next(), false);
411             }
412             if (DEEP_DEBUG)
413                 log.debug("stat after retain: " + getStat());
414         }
415 
416         /**
417          * only used for debugging
418          * 
419          * @return a String representing the sizes of the internal maps
420          */
421         public String getStat() {
422             String s = "keyToMsgObj:" + keyToMsgObj.size() + " hashToMsgObj:"
423                     + hashToMsgObj.size() + " noToMsgObj:" + noToMsgObj.size();
424             return s;
425         }
426 
427         /**
428          * removes a message from the maps.
429          * 
430          * @param key
431          *            key of message
432          * @param decrement
433          *            if true, all message number greater than this are
434          *            decremented
435          */
436         public synchronized void removeByKey(String key, boolean decrement) {
437             MsgObj mo = getByKey(key);
438             if (mo != null) {
439                 keyToMsgObj.remove(mo.key);
440                 noToMsgObj.remove(new Integer(mo.no));
441                 hashToMsgObj.remove(mo.hash);
442                 if (decrement) {
443                     // tailMap finds all entries that have message number
444                     // greater
445                     // than removed one and decrements them
446                     MsgObj[] dmos = (MsgObj[]) noToMsgObj.tailMap(
447                             new Integer(mo.no)).values().toArray(new MsgObj[0]);
448                     for (int i = 0; i < dmos.length; i++) {
449                         MsgObj dmo = dmos[i];
450                         noToMsgObj.remove(new Integer(dmo.no));
451                         dmo.no--;
452                         noToMsgObj.put(new Integer(dmo.no), dmo);
453                     }
454                 }
455             }
456         }
457 
458         /**
459          * Return an String[] of all stored keys
460          * 
461          * @return keys a String[] of all stored keys
462          */
463         public synchronized String[] getKeys() {
464             return (String[]) keyToMsgObj.keySet().toArray(new String[0]);
465         }
466 
467         /**
468          * Return the MsgObj associted with the given key
469          * 
470          * @param key the key 
471          * @return msgObj the MsgObj for the given key
472          */
473         public synchronized MsgObj getByKey(String key) {
474             return (MsgObj) keyToMsgObj.get(key);
475         }
476 
477         /**
478          * At first it tries to lookup message by key otherwise by hash or
479          * stores it as new
480          * 
481          * @param mm
482          *            message
483          * @param key
484          *            if null it will be generated when not found by hash
485          * @param no
486          *            current number of this message
487          * @return fetched/created MsgObj
488          * @throws MessagingException
489          */
490 
491         public synchronized MsgObj put(final MimeMessage mm, String key,
492                 final int no) throws MessagingException {
493             final Object hash = calcMessageHash(mm);
494             MsgObj mo;
495             if (key != null) {
496                 mo = getMsgObj(key);
497             } else {
498                 mo = (MsgObj) hashToMsgObj.get(hash);
499                 if (mo == null) {
500                     key = generateKey(hash.toString());
501                 }
502             }
503             if (mo == null) {
504                 mo = new MsgObj();
505                 keyToMsgObj.put(key, mo);
506                 mo.key = key;
507             }
508             if (!hash.equals(mo.hash)) {
509                 if (mo.hash != null) {
510                     hashToMsgObj.remove(mo.hash);
511                 }
512                 mo.hash = hash;
513                 hashToMsgObj.put(hash, mo);
514             }
515             if (no != mo.no) {
516                 if (mo.no > -1) {
517                     noToMsgObj.remove(new Integer(mo.no));
518                 }
519                 mo.no = no;
520                 noToMsgObj.put(new Integer(no), mo);
521             }
522             if (cacheMessages) {
523                 mo.message = mm;
524             }
525             return mo;
526 
527         }
528 
529         /**
530          * TODO: Check why we have to methods which do the same
531          * 
532          * @see #getByKey(String)
533          */
534         public synchronized MsgObj getMsgObj(String key) {
535             return (MsgObj) keyToMsgObj.get(key);
536         }
537 
538     }
539 
540     /**
541      * used to internal represent a message
542      * 
543      */
544     protected static final class MsgObj {
545         MimeMessage message;
546 
547         int no = -1;
548 
549         Object hash;
550 
551         String key;
552     }
553 
554 
555     /**
556      * currently uses Arrays.hashCode to build an Integer. Resulting Method
557      * should provide a good hashCode and a correct equals method
558      * 
559      * @param mm
560      *            message to hash
561      * @return an Object provides and correct equals method.
562      * @throws MessagingException
563      */  
564     protected static Object calcMessageHash(MimeMessage mm)
565             throws MessagingException {
566         HasherOutputStream hos = new HasherOutputStream();
567         try {
568             mm.writeTo(new CRLFOutputStream(hos));
569         } catch (IOException e) {
570             throw new MessagingException("error while calculating hash", e);
571         }
572 
573         Integer i = new Integer(hos.getHash());
574         return i;
575     }
576 
577     /**
578      * builds a key for unknow messages
579      * 
580      * @param hash Hash to be included in key
581      * @return contains "james-hashed:", the hash, the time and a random long
582      */
583     protected static String generateKey(String hash) {
584         String key = "james-hashed:" + hash + ";" + System.currentTimeMillis()
585                 + ";" + getRandom().nextLong();
586         return key;
587     }
588 
589     /**
590      * just uses a FolderAdapter to wrap around folder
591      * 
592      */
593     public FolderInterface createAdapter(Folder folder) {
594         return new FolderAdapter(folder);
595     }
596 
597 }