Source code: com/eireneh/bible/passage/RangedPassage.java
1
2 package com.eireneh.bible.passage;
3
4 import java.io.*;
5 import java.beans.*;
6 import java.util.*;
7
8 import com.sun.java.util.collections.*;
9 import java.util.NoSuchElementException;
10
11 import com.eireneh.util.LogicError;
12 import com.eireneh.util.IteratorEnumeration;
13
14 /**
15 * A Passage that is implemented using a TreeSet of VerseRanges.
16 * The attributes of the style are:<ul>
17 * <li>Compact storage of large amounts of data
18 * <li>Fast getName()
19 * <li>Slow manipulation
20 * </ul>
21 *
22 * <p>When to normalize()? This is a slow process, but one that is perhaps
23 * done bit-by-bit instead of killing everything just to do getName().
24 * The options are:<ul>
25 * <li>Before every read</li>
26 * <li>Before reads with a background thread</li>
27 * <li>After every change</li>
28 * <li>After every change with a cacheing scheme</li>
29 * </ul>
30 * I'm not sure which will be best. So I'm starting with 1 and
31 * optimizing later ... Maybe the best is to allow the user to choose?
32 *
33 * <table border='1' cellPadding='3' cellSpacing='0' width="100%">
34 * <tr><td bgColor='white'class='TableRowColor'><font size='-7'>
35 * Distribution Licence:<br />
36 * Project B is free software; you can redistribute it
37 * and/or modify it under the terms of the GNU General Public License,
38 * version 2 as published by the Free Software Foundation.<br />
39 * This program is distributed in the hope that it will be useful,
40 * but WITHOUT ANY WARRANTY; without even the implied warranty of
41 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
42 * General Public License for more details.<br />
43 * The License is available on the internet
44 * <a href='http://www.gnu.org/copyleft/gpl.html'>here</a>, by writing to
45 * <i>Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston,
46 * MA 02111-1307, USA</i>, Or locally at the Licence link below.<br />
47 * The copyright to this program is held by it's authors.
48 * </font></td></tr></table>
49 * @see <a href='http://www.eireneh.com/servlets/Web'>Project B Home</a>
50 * @see docs.Licence
51 * @author Joe Walker
52 * @stereotype container
53 * @version $Id:$
54 */
55 public class RangedPassage extends AbstractPassage
56 {
57 /**
58 * Create an empty RangedPassage. There are no ctors from either Verse
59 * or VerseRange so you need to do new <code>RangedPassage().add(...);</code>
60 */
61 protected RangedPassage()
62 {
63 }
64
65 /**
66 * Create a Verse from a human readable string. The opposite
67 * of getName(), Given any RangedPassage v1, and the following
68 * <code>RangedPassage v2 = new RangedPassage(v1.getName());</code>
69 * Then <code>v1.equals(v2);</code>
70 * Theoretically, since there are many ways of representing a RangedPassage as text
71 * string comparision along the lines of:
72 * <code>v1.getName().equals(v2.getName())</code> could be false.
73 * However since getName() is standardized this will be true.
74 * We don't need to worry about thread safety in a ctor since we don't exist yet.
75 * @param refs A String containing the text of the RangedPassage
76 */
77 protected RangedPassage(String refs) throws NoSuchVerseException
78 {
79 super(refs);
80
81 store = Collections.synchronizedSortedSet(new TreeSet());
82 addVerses(refs);
83 normalize();
84 }
85
86 /**
87 * Get a copy of ourselves. Points to note:
88 * Call clone() not new() on member Objects, and on us.
89 * Do not use Copy Constructors! - they do not inherit well.
90 * Think about this needing to be synchronized
91 * If this is not cloneable then writing cloneable children is harder
92 * @return A complete copy of ourselves
93 * @exception java.lang.CloneNotSupportedException We don't do this but our kids might
94 */
95 public Object clone() throws CloneNotSupportedException
96 {
97 // This gets us a shallow copy
98 RangedPassage copy = (RangedPassage) super.clone();
99
100 // I want to just do the following
101 // copy.store = (SortedSet) store.clone();
102 // However SortedSet is not Clonable so I can't
103 // Watch out for this, I'm not sure if it breaks anything.
104 copy.store = Collections.synchronizedSortedSet(new TreeSet());
105 copy.store.addAll(store);
106
107 return copy;
108 }
109
110 /**
111 * @return the number of VerseRanges in this Passage
112 */
113 public int countRanges()
114 {
115 return store.size();
116 }
117
118 /**
119 * @return the number of Verses in this Passage
120 */
121 public int countVerses()
122 {
123 Enumeration en = rangeElements();
124 int count = 0;
125
126 while (en.hasMoreElements())
127 {
128 VerseRange range = (VerseRange) en.nextElement();
129 count += range.getVerseCount();
130 }
131
132 return count;
133 }
134
135 /**
136 * Iterate over the Verses
137 * @return A list enumerator
138 */
139 public Enumeration verseElements()
140 {
141 return new VerseEnumeration();
142 }
143
144 /**
145 * Iterate over the VerseRanges
146 * @return A list enumerator
147 */
148 public Enumeration rangeElements()
149 {
150 return new IteratorEnumeration(store.iterator());
151 }
152
153 /**
154 * @returns true if this Passage contains no Verses
155 */
156 public boolean isEmpty()
157 {
158 return store.isEmpty();
159 }
160
161 /**
162 * Returns true if this Passage contains the specified Verse.
163 * @param obj Verse whose presence in this Passage is to be tested.
164 * @return true if this Passage contains the specified Verse
165 */
166 public boolean contains(VerseBase obj)
167 {
168 // Even for the conatins(VerseRange) case, the simple
169 // 'return store.contains(that);' will not work because
170 // VerseRanges can contain others but not be equal to them.
171
172 VerseRange that_range = toVerseRange(obj);
173
174 Enumeration en = rangeElements();
175 while (en.hasMoreElements())
176 {
177 VerseRange this_range = (VerseRange) en.nextElement();
178 if (this_range.contains(that_range)) return true;
179 }
180
181 // If it is not a Verse or a VerseRange then it's not here,
182 // this also copes with the searches failing.
183 return false;
184 }
185
186 /**
187 * Ensures that this Passage contains the specified Verse
188 * @param obj Verse whose presence in this Passage is to be ensured.
189 */
190 public void add(VerseBase obj)
191 {
192 optimizeWrites();
193
194 VerseRange that_range = toVerseRange(obj);
195 store.add(that_range);
196
197 normalize();
198
199 // we do an extra check here because the cost of calculating the
200 // params is non-zero an may be wasted
201 if (suppress_events == 0)
202 fireIntervalAdded(this, that_range.getStart(), that_range.getEnd());
203 }
204
205 /**
206 * Removes all of the Verses from this Passage.
207 */
208 public void clear()
209 {
210 optimizeWrites();
211
212 store.clear();
213 fireIntervalRemoved(this, null, null);
214 }
215
216 /**
217 * Removes a single instance of the specified Verse from this Passage
218 * @param obj Verse to be removed from this Passage, if present.
219 */
220 public void remove(VerseBase obj)
221 {
222 optimizeWrites();
223
224 VerseRange that_range = toVerseRange(obj);
225 boolean removed = false;
226
227 // This allows us to modify store which iterating through a copy
228 SortedSet new_store = Collections.synchronizedSortedSet(new TreeSet());
229 new_store.addAll(store);
230 Iterator it = new_store.iterator();
231
232 // go through all the VerseRanges
233 while (it.hasNext())
234 {
235 // if this range touches the range to be removed ...
236 VerseRange this_range = (VerseRange) it.next();
237 if (this_range.overlaps(that_range))
238 {
239 // ... remove it and add the remainder
240 store.remove(this_range);
241 VerseRange[] vra = VerseRange.remainder(this_range, that_range);
242
243 for (int i=0; i<vra.length; i++)
244 store.add(vra[i]);
245
246 removed = true;
247 }
248 }
249
250 if (removed) normalize();
251
252 // we do an extra check here because the cost of calculating the
253 // params is non-zero an may be wasted
254 if (suppress_events == 0)
255 fireIntervalRemoved(this, that_range.getStart(), that_range.getEnd());
256 }
257
258 /**
259 * Retains only the Verses in this Passage that are contained in the
260 * specified Passage
261 * @param that Verses to be retained in this Passage.
262 */
263 public void retainAll(Passage that)
264 {
265 optimizeWrites();
266
267 SortedSet new_store = Collections.synchronizedSortedSet(new TreeSet());
268
269 Enumeration that_en = null;
270 if (that instanceof Passage) that_en = ((Passage) that).rangeElements();
271 else that_en = that.verseElements();
272
273 while (that_en.hasMoreElements())
274 {
275 VerseRange that_range = toVerseRange(that_en.nextElement());
276
277 // go through all the VerseRanges
278 Enumeration this_en = rangeElements();
279 while (this_en.hasMoreElements())
280 {
281 // if this range touches the range to be removed ...
282 VerseRange this_range = (VerseRange) this_en.nextElement();
283 if (this_range.overlaps(that_range))
284 {
285 // ... remove it and add the remainder
286 VerseRange interstect = VerseRange.intersection(this_range, that_range);
287 if (interstect != null) new_store.add(interstect);
288 }
289 }
290 }
291
292 store = new_store;
293 normalize();
294
295 fireIntervalRemoved(this, null, null);
296 }
297
298 /**
299 * We sometimes need to sort ourselves out ...
300 * I don't think we need to be synchronised since we are private
301 * and we could check that all public calling of normalize() are
302 * synchronised, however this is safe, and I don't think there is
303 * a cost associated with a double synchronize. (?)
304 */
305 protected void normalize()
306 {
307 if (skip_normalization != 0) return;
308
309 VerseRange last = null;
310 VerseRange next = null;
311 SortedSet new_store = Collections.synchronizedSortedSet(new TreeSet());
312
313 Enumeration en = rangeElements();
314 while (en.hasMoreElements())
315 {
316 next = (VerseRange) en.nextElement();
317
318 if (last != null && next.adjacentTo(last))
319 {
320 VerseRange merge = new VerseRange(last, next);
321
322 new_store.remove(last);
323 new_store.add(merge);
324
325 last = merge;
326 }
327 else
328 {
329 new_store.add(next);
330 last = next;
331 }
332 }
333
334 store = new_store;
335 }
336
337 /**
338 * This class is here to prevent users of RangedPassage.iterator() from
339 * altering the underlying store and getting us out of sync. Right
340 * now there are no issues with someone else removing a RangedPassage
341 * without telling us, however there may be some day, and I'm not
342 * sure that we need the functionality right now.
343 * Also buy using this we get to ensure synchronization.
344 * Everything is final so to save the proxying performace hit.
345 * @author Joe Walker
346 */
347 private final class VerseEnumeration implements Enumeration
348 {
349 /**
350 * Create a basic iterator that is a proxy for the RangedPassage Passages
351 * iterator, with remove() overridden.
352 */
353 public VerseEnumeration()
354 {
355 try
356 {
357 SortedSet temp = Collections.synchronizedSortedSet(new TreeSet());
358 Enumeration en = rangeElements();
359
360 while (en.hasMoreElements())
361 {
362 VerseRange range = (VerseRange) en.nextElement();
363
364 for (int i=0; i<range.getVerseCount(); i++)
365 {
366 temp.add(new Verse(range.getStart().getOrdinal()+i));
367 }
368 }
369
370 real = temp.iterator();
371 }
372 catch (NoSuchVerseException ex)
373 {
374 throw new LogicError(ex);
375 }
376 }
377
378 /**
379 * Returns true if the iteration has more Verses.
380 * Pass the request on to the real iterator.
381 */
382 public final boolean hasMoreElements()
383 {
384 return real.hasNext();
385 }
386
387 /**
388 * Returns the next Verse in the interation.
389 * Pass the request on to the real iterator.
390 */
391 public final Object nextElement() throws NoSuchElementException
392 {
393 return real.next();
394 }
395
396 /** The Iterator that we are proxying to */
397 private Iterator real;
398 }
399
400 /**
401 * Call the support mechanism in AbstractPassage
402 * @param out The stream to write our state to
403 * @serialData Write the ordinal number of this verse
404 * @see AbstractPassage#writeObjectSupport(ObjectOutputStream)
405 */
406 private void writeObject(ObjectOutputStream out) throws IOException
407 {
408 writeObjectSupport(out);
409 }
410
411 /**
412 * Call the support mechanism in AbstractPassage
413 * @param in The stream to read our state from
414 * @serialData Write the ordinal number of this verse
415 * @see AbstractPassage#readObjectSupport(ObjectInputStream)
416 */
417 private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
418 {
419 optimizeWrites();
420
421 store = Collections.synchronizedSortedSet(new TreeSet());
422 readObjectSupport(in);
423 }
424
425 /** To make serialization work across new versions */
426 static final long serialVersionUID = 955115811339960826L;
427
428 /** The place the real data is stored */
429 private transient SortedSet store = Collections.synchronizedSortedSet(new TreeSet());
430 }