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.Iterator;
024    import java.util.List;
025    
026    import javax.mail.Folder;
027    import javax.mail.MessagingException;
028    
029    import org.apache.geronimo.javamail.store.nntp.newsrc.NNTPNewsrcGroup;
030    import org.apache.geronimo.javamail.transport.nntp.NNTPReply;
031    import org.apache.geronimo.mail.util.SessionUtil;
032    
033    /**
034     * The base NNTP implementation of the javax.mail.Folder This is a base class
035     * for both the Root NNTP server and each NNTP group folder.
036     * 
037     * @see javax.mail.Folder
038     * 
039     * @version $Rev: 437941 $
040     */
041    public class NNTPRootFolder extends NNTPFolder {
042        protected static final String NNTP_LISTALL = "mail.nntp.listall";
043    
044        /**
045         * Construct the NNTPRootFolder.
046         * 
047         * @param store
048         *            The owning Store.
049         * @param name
050         *            The folder name (by default, this is the server host name).
051         * @param fullName
052         *            The fullName to use for this server (derived from welcome
053         *            string).
054         */
055        protected NNTPRootFolder(NNTPStore store, String name, String fullName) {
056            super(store);
057    
058            this.name = name;
059            this.fullName = fullName;
060        }
061    
062        /**
063         * List the subfolders. For group folders, this is a meaningless so we throw
064         * a MethodNotSupportedException.
065         * 
066         * @param pattern
067         *            The folder pattern string.
068         * 
069         * @return Never returns.
070         * @exception MessagingException
071         */
072        public synchronized Folder[] list(String pattern) throws MessagingException {
073            // the pattern specfied for javamail uses two wild card characters, "%"
074            // and "*". The "%" matches
075            // and character except hierarchy separators. Since we have a flag
076            // hierarchy, "%" and "*" are
077            // essentially the same. If we convert the "%" into "*", we can just
078            // treat this as a wildmat
079            // formatted pattern and pass this on to the server rather than having
080            // to read everything and
081            // process the strings on the client side.
082    
083            pattern = pattern.replace('%', '*');
084    
085            // if we're not supposed to list everything, then just filter the list
086            // of subscribed groups.
087            if (SessionUtil.getBooleanProperty(NNTP_LISTALL, false)) {
088                return filterActiveGroups(pattern);
089            } else {
090                return filterSubscribedGroups(pattern);
091            }
092        }
093    
094        /**
095         * Retrieve the list of subscribed folders that match the given pattern
096         * string.
097         * 
098         * @param pattern
099         *            The pattern string used for the matching
100         * 
101         * @return An array of matching folders from the subscribed list.
102         */
103        public Folder[] listSubscribed(String pattern) throws MessagingException {
104            // the pattern specfied for javamail uses two wild card characters, "%"
105            // and "*". The "%" matches
106            // and character except hierarchy separators. Since we have a flag
107            // hierarchy, "%" and "*" are
108            // essentially the same. If we convert the "%" into "*", we can just
109            // treat this as a wildmat
110            // formatted pattern and pass this on to the server rather than having
111            // to read everything and
112            // process the strings on the client side.
113    
114            pattern = pattern.replace('%', '*');
115    
116            return filterSubscribedGroups(pattern);
117        }
118    
119        /**
120         * Retrieve the list of matching groups from the NNTP server using the LIST
121         * ACTIVE command. The server does the wildcard matching for us.
122         * 
123         * @param pattern
124         *            The pattern string (in wildmat format) used to match.
125         * 
126         * @return An array of folders for the matching groups.
127         */
128        protected Folder[] filterActiveGroups(String pattern) throws MessagingException {
129            NNTPReply reply = connection.sendCommand("LIST ACTIVE " + pattern, NNTPReply.LIST_FOLLOWS);
130    
131            // if the LIST ACTIVE command isn't supported,
132            if (reply.getCode() == NNTPReply.COMMAND_NOT_RECOGNIZED) {
133                // only way to list all is to retrieve all and filter.
134                return filterAllGroups(pattern);
135            } else if (reply.getCode() != NNTPReply.LIST_FOLLOWS) {
136                throw new MessagingException("Error retrieving group list from NNTP server: " + reply);
137            }
138    
139            // get the response back from the server and process each returned group
140            // name.
141            List groups = reply.getData();
142    
143            Folder[] folders = new Folder[groups.size()];
144            for (int i = 0; i < groups.size(); i++) {
145                folders[i] = getFolder(getGroupName((String) groups.get(i)));
146            }
147            return folders;
148        }
149    
150        /**
151         * Retrieve a list of all groups from the server and filter on the names.
152         * Not recommended for the usenet servers, as there are over 30000 groups to
153         * process.
154         * 
155         * @param pattern
156         *            The pattern string used for the selection.
157         * 
158         * @return The Folders for the matching groups.
159         */
160        protected Folder[] filterAllGroups(String pattern) throws MessagingException {
161            NNTPReply reply = connection.sendCommand("LIST", NNTPReply.LIST_FOLLOWS);
162    
163            if (reply.getCode() != NNTPReply.LIST_FOLLOWS) {
164                throw new MessagingException("Error retrieving group list from NNTP server: " + reply);
165            }
166    
167            // get the response back from the server and process each returned group
168            // name.
169            List groups = reply.getData();
170    
171            WildmatMatcher matcher = new WildmatMatcher(pattern);
172    
173            List folders = new ArrayList();
174            for (int i = 0; i < groups.size(); i++) {
175                String name = getGroupName((String) groups.get(i));
176                // does this match our pattern? Add to the list
177                if (matcher.matches(name)) {
178                    folders.add(getFolder(name));
179                }
180            }
181            return (Folder[]) folders.toArray(new Folder[0]);
182        }
183    
184        /**
185         * Return the set of groups from the newsrc subscribed groups list that
186         * match a given filter.
187         * 
188         * @param pattern
189         *            The selection pattern.
190         * 
191         * @return The Folders for the matching groups.
192         */
193        protected Folder[] filterSubscribedGroups(String pattern) throws MessagingException {
194            Iterator groups = ((NNTPStore) store).getNewsrcGroups();
195    
196            WildmatMatcher matcher = new WildmatMatcher(pattern);
197    
198            List folders = new ArrayList();
199            while (groups.hasNext()) {
200                NNTPNewsrcGroup group = (NNTPNewsrcGroup) groups.next();
201                if (group.isSubscribed()) {
202                    // does this match our pattern? Add to the list
203                    if (matcher.matches(group.getName())) {
204                        folders.add(getFolder(group.getName()));
205                    }
206                }
207            }
208            return (Folder[]) folders.toArray(new Folder[0]);
209        }
210    
211        /**
212         * Utility method for extracting a name from a group list response.
213         * 
214         * @param response
215         *            The response string.
216         * 
217         * @return The group name.
218         */
219        protected String getGroupName(String response) {
220            int blank = response.indexOf(' ');
221            return response.substring(0, blank).trim();
222        }
223    
224        /**
225         * Return whether this folder can hold just messages or also subfolders.
226         * Only the root folder can hold other folders, so it will need to override.
227         * 
228         * @return Always returns Folder.HOLDS_FOLDERS.
229         * @exception MessagingException
230         */
231        public int getType() throws MessagingException {
232            return HOLDS_FOLDERS;
233        }
234    
235        /**
236         * Get a new folder from the root folder. This creates a new folder, which
237         * might not actually exist on the server. If the folder doesn't exist, an
238         * error will occur on folder open.
239         * 
240         * @param name
241         *            The name of the requested folder.
242         * 
243         * @return A new folder object for this folder.
244         * @exception MessagingException
245         */
246        public Folder getFolder(String name) throws MessagingException {
247            // create a new group folder and return
248            return new NNTPGroupFolder(this, (NNTPStore) store, name, ((NNTPStore) store).getNewsrcGroup(name));
249        }
250    
251        /**
252         * Utility class to do Wildmat pattern matching on folder names.
253         */
254        class WildmatMatcher {
255            // middle match sections...because these are separated by wildcards, if
256            // they appear in
257            // sequence in the string, it is a match.
258            List matchSections = new ArrayList();
259    
260            // just a "*" match, so everything is true
261            boolean matchAny = false;
262    
263            // no wildcards, so this must be an exact match.
264            String exactMatch = null;
265    
266            // a leading section which must be at the beginning
267            String firstSection = null;
268    
269            // a trailing section which must be at the end of the string.
270            String lastSection = null;
271    
272            /**
273             * Create a wildmat pattern matcher.
274             * 
275             * @param pattern
276             *            The wildmat pattern to apply to string matches.
277             */
278            public WildmatMatcher(String pattern) {
279                int section = 0;
280    
281                // handle the easy cases first
282    
283                // single wild card?
284                if (pattern.equals("*")) {
285                    matchAny = true;
286                    return;
287                }
288    
289                // find the first wild card
290                int wildcard = pattern.indexOf('*');
291    
292                // no wild card at all?
293                if (wildcard == -1) {
294                    exactMatch = pattern;
295                    return;
296                }
297    
298                // pattern not begin with a wildcard? We need to pull off the
299                // leading section
300                if (!pattern.startsWith("*")) {
301                    firstSection = pattern.substring(0, wildcard);
302                    section = wildcard + 1;
303                    // this could be "yada*", so we could be done.
304                    if (section >= pattern.length()) {
305                        return;
306                    }
307                }
308    
309                // now parse off the middle sections, making sure to handle the end
310                // condition correctly.
311                while (section < pattern.length()) {
312                    // find the next wildcard position
313                    wildcard = pattern.indexOf('*', section);
314                    if (wildcard == -1) {
315                        // not found, we're at the end of the pattern. We need to
316                        // match on the end.
317                        lastSection = pattern.substring(section);
318                        return;
319                    }
320                    // we could have a null section, which we'll just ignore.
321                    else if (wildcard == section) {
322                        // step over the wild card
323                        section++;
324                    } else {
325                        // pluck off the next section
326                        matchSections.add(pattern.substring(section, wildcard));
327                        // step over the wild card character and check if we've
328                        // reached the end.
329                        section = wildcard + 1;
330                    }
331                }
332            }
333    
334            /**
335             * Test if a name string matches to parsed wildmat pattern.
336             * 
337             * @param name
338             *            The name to test.
339             * 
340             * @return true if the string matches the pattern, false otherwise.
341             */
342            public boolean matches(String name) {
343    
344                // handle the easy cases first
345    
346                // full wildcard? Always matches
347                if (matchAny) {
348                    return true;
349                }
350    
351                // required exact matches are easy.
352                if (exactMatch != null) {
353                    return exactMatch.equals(name);
354                }
355    
356                int span = 0;
357    
358                // must match the beginning?
359                if (firstSection != null) {
360                    // if it doesn't start with that, it can't be true.
361                    if (!name.startsWith(firstSection)) {
362                        return false;
363                    }
364    
365                    // we do all additional matching activity from here.
366                    span = firstSection.length();
367                }
368    
369                // scan for each of the sections along the string
370                for (int i = 1; i < matchSections.size(); i++) {
371                    // if a section is not found, this is false
372    
373                    String nextMatch = (String) matchSections.get(i);
374                    int nextLocation = name.indexOf(nextMatch, span);
375                    if (nextLocation == -1) {
376                        return false;
377                    }
378                    // step over that one
379                    span = nextMatch.length() + nextLocation;
380                }
381    
382                // we've matched everything up to this point, now check to see if
383                // need an end match
384                if (lastSection != null) {
385                    // we need to have at least the number of characters of the end
386                    // string left, else this fails.
387                    if (name.length() - span < lastSection.length()) {
388                        return false;
389                    }
390    
391                    // ok, make sure we end with this string
392                    return name.endsWith(lastSection);
393                }
394    
395                // no falsies, this must be the truth.
396                return true;
397            }
398        }
399    }