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.geronimo.javamail.store.nntp;
21  
22  import java.util.ArrayList;
23  import java.util.HashMap;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.StringTokenizer;
27  
28  import javax.mail.FetchProfile;
29  import javax.mail.FolderNotFoundException;
30  import javax.mail.Message;
31  import javax.mail.MessagingException;
32  
33  import org.apache.geronimo.javamail.store.nntp.newsrc.NNTPNewsrcGroup;
34  import org.apache.geronimo.javamail.transport.nntp.NNTPReply;
35  
36  /**
37   * The NNTP implementation of the javax.mail.Folder Note that only INBOX is
38   * supported in NNTP
39   * <p>
40   * <url>http://www.faqs.org/rfcs/rfc1939.html</url>
41   * </p>
42   * 
43   * @see javax.mail.Folder
44   * 
45   * @version $Rev: 686231 $ $Date: 2008-08-15 10:24:20 -0400 (Fri, 15 Aug 2008) $
46   */
47  public class NNTPGroupFolder extends NNTPFolder {
48  
49      // holders for status information returned by the GROUP command.
50      protected int firstArticle = -1;
51  
52      protected int lastArticle = -1;
53  
54      // retrieved articles, mapped by article number.
55      Map articles;
56  
57      // information stored in the newsrc group.
58      NNTPNewsrcGroup groupInfo;
59  
60      /**
61       * Construct a "real" folder representing an NNTP news group.
62       * 
63       * @param parent
64       *            The parent root folder.
65       * @param store
66       *            The Store this folder is attached to.
67       * @param name
68       *            The folder name.
69       * @param groupInfo
70       *            The newsrc group information attached to the newsrc database.
71       *            This contains subscription and article "SEEN" information.
72       */
73      protected NNTPGroupFolder(NNTPRootFolder parent, NNTPStore store, String name, NNTPNewsrcGroup groupInfo) {
74          super(store);
75          // the name and the full name are the same.
76          this.name = name;
77          this.fullName = name;
78          // set the parent appropriately.
79          this.parent = parent = parent;
80          this.groupInfo = groupInfo;
81      }
82  
83      /**
84       * Ping the server and update the group count, first, and last information.
85       * 
86       * @exception MessagingException
87       */
88      private void updateGroupStats() throws MessagingException {
89          // ask the server for information about the group. This is a one-line
90          // reponse with status on
91          // the group, if it exists.
92          NNTPReply reply = connection.sendCommand("GROUP " + name);
93  
94          // explicitly not there?
95          if (reply.getCode() == NNTPReply.NO_SUCH_NEWSGROUP) {
96              throw new FolderNotFoundException(this, "Folder does not exist on server: " + reply);
97          } else if (reply.getCode() != NNTPReply.GROUP_SELECTED) {
98              throw new MessagingException("Error requesting group information: " + reply);
99          }
100 
101         // we've gotten back a good response, now parse out the group specifics
102         // from the
103         // status response.
104 
105         StringTokenizer tokenizer = new StringTokenizer(reply.getMessage());
106 
107         // we should have a least 3 tokens here, in the order "count first
108         // last".
109 
110         // article count
111         if (tokenizer.hasMoreTokens()) {
112             String count = tokenizer.nextToken();
113             try {
114                 messageCount = Integer.parseInt(count);
115             } catch (NumberFormatException e) {
116                 // ignore
117             }
118         }
119 
120         // first article number
121         if (tokenizer.hasMoreTokens()) {
122             String first = tokenizer.nextToken();
123             try {
124                 firstArticle = Integer.parseInt(first);
125             } catch (NumberFormatException e) {
126                 // ignore
127             }
128         }
129 
130         // last article number.
131         if (tokenizer.hasMoreTokens()) {
132             String last = tokenizer.nextToken();
133             try {
134                 lastArticle = Integer.parseInt(last);
135             } catch (NumberFormatException e) {
136                 // ignore
137             }
138         }
139     }
140 
141     /**
142      * Test to see if this folder actually exists. This pings the server for
143      * information about the GROUP and updates the article count and index
144      * information.
145      * 
146      * @return true if the newsgroup exists on the server, false otherwise.
147      * @exception MessagingException
148      */
149     public boolean exists() throws MessagingException {
150 
151         try {
152             // update the group statistics. If the folder doesn't exist, we'll
153             // get an exception that we
154             // can turn into a false reply.
155             updateGroupStats();
156             // updated ok, so it must be there.
157             return true;
158         } catch (FolderNotFoundException e) {
159             return false;
160         }
161     }
162 
163     /**
164      * Ping the NNTP server to check if a newsgroup has any new messages.
165      * 
166      * @return True if the server has new articles from the last time we
167      *         checked. Also returns true if this is the first time we've
168      *         checked.
169      * @exception MessagingException
170      */
171     public boolean hasNewMessages() throws MessagingException {
172         int oldLast = lastArticle;
173         updateGroupStats();
174 
175         return lastArticle > oldLast;
176     }
177 
178     /**
179      * Open the folder for use. This retrieves article count information from
180      * the server.
181      * 
182      * @exception MessagingException
183      */
184     protected void openFolder() throws MessagingException {
185         // update the group specifics, especially the message count.
186         updateGroupStats();
187 
188         // get a cache for retrieved articles
189         articles = new HashMap();
190     }
191 
192     /**
193      * Close the folder, which also clears out the article caches.
194      * 
195      * @exception MessagingException
196      */
197     public void closeFolder() throws MessagingException {
198         // get ride of any retrieve articles, and flip over the open for
199         // business sign.
200         articles = null;
201     }
202 
203     /**
204      * Checks wether the message is in cache, if not will create a new message
205      * object and return it.
206      * 
207      * @see javax.mail.Folder#getMessage(int)
208      */
209     public Message getMessage(int msgNum) throws MessagingException {
210         // Can only be performed on an Open folder
211         checkOpen();
212 
213         // get an object form to look up in the retrieve messages list (oh how I
214         // wish there was
215         // something like Map that could use integer keys directly!).
216         Integer key = new Integer(msgNum);
217         NNTPMessage message = (NNTPMessage) articles.get(key);
218         if (message != null) {
219             // piece of cake!
220             return message;
221         }
222 
223         // we need to suck a message down from the server.
224         // but first, make sure the group is still valid.
225         updateGroupStats();
226 
227         // just send a STAT command to this message. Right now, all we want is
228         // existance proof. We'll
229         // retrieve the other bits when requested.
230         NNTPReply reply = connection.sendCommand("STAT " + Integer.toString(msgNum));
231         if (reply.getCode() != NNTPReply.REQUEST_TEXT_SEPARATELY) {
232             throw new MessagingException("Error retrieving article from NNTP server: " + reply);
233         }
234 
235         // we need to parse out the message id.
236         String response = reply.getMessage();
237 
238         int idStart = response.indexOf('<');
239         int idEnd = response.indexOf('>');
240 
241         // NB:  The "<" and ">" delimiters are required elements of the message id, not just 
242         // delimiters for the sake of the command.  We need to keep these around 
243         message = new NNTPMessage(this, (NNTPStore) store, msgNum, response.substring(idStart, idEnd + 1));
244 
245         // add this to the article cache.
246         articles.put(key, message);
247 
248         return message;
249     }
250 
251     /**
252      * Retrieve all articles in the group.
253      * 
254      * @return An array of all messages in the group.
255      */
256     public Message[] getMessages() throws MessagingException {
257         // Can only be performed on an Open folder
258         checkOpen();
259         
260         // we're going to try first with XHDR, which will allow us to retrieve
261         // everything in one shot. If that
262         // fails, we'll fall back on issing STAT commands for the entire article
263         // range.
264         NNTPReply reply = connection.sendCommand("XHDR Message-ID " + Integer.toString(firstArticle) + "-"
265                 + Integer.toString(lastArticle), NNTPReply.HEAD_FOLLOWS);
266 
267         List messages = new ArrayList();
268 
269         if (reply.getCode() == NNTPReply.HEAD_FOLLOWS) {
270             List lines = reply.getData();
271 
272             for (int i = 0; i < lines.size(); i++) {
273                 String line = (String) lines.get(i);
274 
275                 try {
276                     int pos = line.indexOf(' ');
277                     int articleID = Integer.parseInt(line.substring(0, pos));
278                     String messageID = line.substring(pos + 1);
279                     Integer key = new Integer(articleID);
280                     // see if we have this message cached, If not, create it.
281                     Message message = (Message)articles.get(key);
282                     if (message == null) {
283                         message = new NNTPMessage(this, (NNTPStore) store, key.intValue(), messageID);
284                         articles.put(key, message);
285                     }
286 
287                     messages.add(message);
288 
289                 } catch (NumberFormatException e) {
290                     // should never happen, but just skip this entry if it does.
291                 }
292             }
293         } else {
294             // grumble, we need to stat each article id to see if it
295             // exists....lots of round trips.
296             for (int i = firstArticle; i <= lastArticle; i++) {
297                 try {
298                     messages.add(getMessage(i));
299                 } catch (MessagingException e) {
300                     // just assume if there is an error, it's because the
301                     // message id doesn't exist.
302                 }
303             }
304         }
305 
306         return (Message[]) messages.toArray(new Message[0]);
307     }
308 
309     /**
310      * @see javax.mail.Folder#fetch(javax.mail.Message[],
311      *      javax.mail.FetchProfile)
312      * 
313      * The JavaMail API recommends that this method be overrident to provide a
314      * meaningfull implementation.
315      */
316     public void fetch(Message[] msgs, FetchProfile fp) throws MessagingException {
317         // Can only be performed on an Open folder
318         checkOpen();
319 
320         for (int i = 0; i < msgs.length; i++) {
321             Message msg = msgs[i];
322             // we can only perform this operation for NNTPMessages.
323             if (msg == null || !(msg instanceof NNTPMessage)) {
324                 // we can't fetch if it's the wrong message type
325                 continue;
326             }
327 
328             // fetching both the headers and body?
329             if (fp.contains(FetchProfile.Item.ENVELOPE) && fp.contains(FetchProfile.Item.CONTENT_INFO)) {
330 
331                 // retrive everything
332                 ((NNTPMessage) msg).loadArticle();
333             }
334             // headers only?
335             else if (fp.contains(FetchProfile.Item.ENVELOPE)) {
336                 ((NNTPMessage) msg).loadHeaders();
337             } else if (fp.contains(FetchProfile.Item.CONTENT_INFO)) {
338                 ((NNTPMessage) msg).loadContent();
339             }
340         }
341     }
342 
343     /**
344      * Return the subscription status of this folder.
345      * 
346      * @return true if the folder is marked as subscribed, false for
347      *         unsubscribed.
348      */
349     public boolean isSubscribed() {
350         return groupInfo.isSubscribed();
351     }
352 
353     /**
354      * Set or clear the subscription status of a file.
355      * 
356      * @param flag
357      *            The new subscription state.
358      */
359     public void setSubscribed(boolean flag) {
360         groupInfo.setSubscribed(flag);
361     }
362 
363     /**
364      * Return the "seen" state for an article in a folder.
365      * 
366      * @param article
367      *            The article number.
368      * 
369      * @return true if the article is marked as seen in the newsrc file, false
370      *         for unseen files.
371      */
372     public boolean isSeen(int article) {
373         return groupInfo.isArticleSeen(article);
374     }
375 
376     /**
377      * Set the seen state for an article in a folder.
378      * 
379      * @param article
380      *            The article number.
381      * @param flag
382      *            The new seen state.
383      */
384     public void setSeen(int article, boolean flag) {
385         if (flag) {
386             groupInfo.markArticleSeen(article);
387         } else {
388             groupInfo.markArticleUnseen(article);
389         }
390     }
391 }