001 /*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements. See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership. The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License. You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied. See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019
020 package org.apache.geronimo.javamail.store.nntp;
021
022 import java.util.ArrayList;
023 import java.util.HashMap;
024 import java.util.List;
025 import java.util.Map;
026 import java.util.StringTokenizer;
027
028 import javax.mail.FetchProfile;
029 import javax.mail.FolderNotFoundException;
030 import javax.mail.Message;
031 import javax.mail.MessagingException;
032
033 import org.apache.geronimo.javamail.store.nntp.newsrc.NNTPNewsrcGroup;
034 import org.apache.geronimo.javamail.transport.nntp.NNTPReply;
035
036 /**
037 * The NNTP implementation of the javax.mail.Folder Note that only INBOX is
038 * supported in NNTP
039 * <p>
040 * <url>http://www.faqs.org/rfcs/rfc1939.html</url>
041 * </p>
042 *
043 * @see javax.mail.Folder
044 *
045 * @version $Rev: 686231 $ $Date: 2008-08-15 10:24:20 -0400 (Fri, 15 Aug 2008) $
046 */
047 public class NNTPGroupFolder extends NNTPFolder {
048
049 // holders for status information returned by the GROUP command.
050 protected int firstArticle = -1;
051
052 protected int lastArticle = -1;
053
054 // retrieved articles, mapped by article number.
055 Map articles;
056
057 // information stored in the newsrc group.
058 NNTPNewsrcGroup groupInfo;
059
060 /**
061 * Construct a "real" folder representing an NNTP news group.
062 *
063 * @param parent
064 * The parent root folder.
065 * @param store
066 * The Store this folder is attached to.
067 * @param name
068 * The folder name.
069 * @param groupInfo
070 * The newsrc group information attached to the newsrc database.
071 * This contains subscription and article "SEEN" information.
072 */
073 protected NNTPGroupFolder(NNTPRootFolder parent, NNTPStore store, String name, NNTPNewsrcGroup groupInfo) {
074 super(store);
075 // the name and the full name are the same.
076 this.name = name;
077 this.fullName = name;
078 // set the parent appropriately.
079 this.parent = parent = parent;
080 this.groupInfo = groupInfo;
081 }
082
083 /**
084 * Ping the server and update the group count, first, and last information.
085 *
086 * @exception MessagingException
087 */
088 private void updateGroupStats() throws MessagingException {
089 // ask the server for information about the group. This is a one-line
090 // reponse with status on
091 // the group, if it exists.
092 NNTPReply reply = connection.sendCommand("GROUP " + name);
093
094 // explicitly not there?
095 if (reply.getCode() == NNTPReply.NO_SUCH_NEWSGROUP) {
096 throw new FolderNotFoundException(this, "Folder does not exist on server: " + reply);
097 } else if (reply.getCode() != NNTPReply.GROUP_SELECTED) {
098 throw new MessagingException("Error requesting group information: " + reply);
099 }
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 }