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.newsrc;
021    
022    import java.io.IOException;
023    import java.io.Writer;
024    import java.util.ArrayList;
025    import java.util.StringTokenizer;
026    
027    /**
028     * Manage a list of ranges values from a newsrc file.
029     */
030    public class RangeList {
031        boolean dirty = false;
032    
033        ArrayList ranges = new ArrayList();
034    
035        /**
036         * Create a RangeList instance from a newsrc range line. Values are saved as
037         * a comma separated set of range values. A range value is either a single
038         * number or a hypenated pair of numbers.
039         * 
040         * @param line
041         *            The newsrc range line.
042         */
043        public RangeList(String line) {
044    
045            // we might be creating an first time list, so nothing to parse.
046            if (line != null) {
047                // ranges are comma delimited tokens
048                StringTokenizer tokenizer = new StringTokenizer(line, ",");
049    
050                while (tokenizer.hasMoreTokens()) {
051                    String rangeString = (String) tokenizer.nextToken();
052                    rangeString = rangeString.trim();
053                    if (rangeString.length() != 0) {
054                        Range range = Range.parse(rangeString);
055                        if (range != null) {
056                            insert(range);
057                        }
058                    }
059                }
060            }
061            // make sure we start out in a clean state. Any changes from this point
062            // will flip on the dirty flat.
063            dirty = false;
064        }
065    
066        /**
067         * Insert a range item into our list. If possible, the inserted range will
068         * be merged with existing ranges.
069         * 
070         * @param newRange
071         *            The new range item.
072         */
073        public void insert(Range newRange) {
074            // first find the insertion point
075            for (int i = 0; i < ranges.size(); i++) {
076                Range range = (Range) ranges.get(i);
077                // does an existing range fully contain the new range, we don't need
078                // to insert anything.
079                if (range.contains(newRange)) {
080                    return;
081                }
082    
083                // does the current one abutt or overlap with the inserted range?
084                if (range.abutts(newRange) || range.overlaps(newRange)) {
085                    // rats, we have an overlap...and it is possible that we could
086                    // overlap with
087                    // the next range after this one too. Therefore, we merge these
088                    // two ranges together,
089                    // remove the place where we're at, and then recursively insert
090                    // the larger range into
091                    // the list.
092                    dirty = true;
093                    newRange.merge(range);
094                    ranges.remove(i);
095                    insert(newRange);
096                    return;
097                }
098    
099                // ok, we don't touch the current one at all. If it is completely
100                // above
101                // range we're adding, we can just poke this into the list here.
102                if (newRange.lessThan(range)) {
103                    dirty = true;
104                    ranges.add(i, newRange);
105                    return;
106                }
107            }
108            dirty = true;
109            // this is easy (and fairly common)...we just tack this on to the end.
110            ranges.add(newRange);
111        }
112    
113        /**
114         * Test if a given article point falls within one of the contained Ranges.
115         * 
116         * @param article
117         *            The test point.
118         * 
119         * @return true if this falls within one of our current mark Ranges, false
120         *         otherwise.
121         */
122        public boolean isMarked(int article) {
123            for (int i = 0; i < ranges.size(); i++) {
124                Range range = (Range) ranges.get(i);
125                if (range.contains(article)) {
126                    return true;
127                }
128                // we've passed the point where a match is possible.
129                if (range.greaterThan(article)) {
130                    return false;
131                }
132            }
133            return false;
134        }
135    
136        /**
137         * Mark a target article as having been seen.
138         * 
139         * @param article
140         *            The target article number.
141         */
142        public void setMarked(int article) {
143            // go through the insertion logic.
144            insert(new Range(article, article));
145        }
146    
147        /**
148         * Clear the seen mark for a target article.
149         * 
150         * @param article
151         *            The target article number.
152         */
153        public void setUnmarked(int article) {
154            for (int i = 0; i < ranges.size(); i++) {
155                Range range = (Range) ranges.get(i);
156                // does this fall within an existing range? We don't need to do
157                // anything here.
158                if (range.contains(article)) {
159                    // ok, we've found where to insert, now to figure out how to
160                    // insert
161                    // article is at the beginning of the range. We can just
162                    // increment the lower
163                    // bound, or if this is a single element range, we can remove it
164                    // entirely.
165                    if (range.getStart() == article) {
166                        if (range.getEnd() == article) {
167                            // piece of cake!
168                            ranges.remove(i);
169                        } else {
170                            // still pretty easy.
171                            range.setStart(article + 1);
172                        }
173                    } else if (range.getEnd() == article) {
174                        // pretty easy also
175                        range.setEnd(article - 1);
176                    } else {
177                        // split this into two ranges and insert the trailing piece
178                        // after this.
179                        Range section = range.split(article);
180                        ranges.add(i + 1, section);
181                    }
182                    dirty = true;
183                    return;
184                }
185                // did we find a point where any articles are greater?
186                if (range.greaterThan(article)) {
187                    // nothing to do
188                    return;
189                }
190            }
191            // didn't find it at all. That was easy!
192        }
193    
194        /**
195         * Save this List of Ranges out to a .newsrc file. This creates a
196         * comma-separated list of range values from each of the Ranges.
197         * 
198         * @param out
199         *            The target output stream.
200         * 
201         * @exception IOException
202         */
203        public void save(Writer out) throws IOException {
204            // we have an empty list
205            if (ranges.size() == 0) {
206                return;
207            }
208    
209            Range range = (Range) ranges.get(0);
210            range.save(out);
211    
212            for (int i = 1; i < ranges.size(); i++) {
213                out.write(",");
214                range = (Range) ranges.get(i);
215                range.save(out);
216            }
217        }
218    
219        /**
220         * Return the state of the dirty flag.
221         * 
222         * @return True if the range list information has changed, false otherwise.
223         */
224        public boolean isDirty() {
225            return dirty;
226        }
227    }