1   /**
2    * Distribution License:
3    * JSword is free software; you can redistribute it and/or modify it under
4    * the terms of the GNU Lesser General Public License, version 2.1 or later
5    * as published by the Free Software Foundation. This program is distributed
6    * in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
7    * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
8    * See the GNU Lesser General Public License for more details.
9    *
10   * The License is available on the internet at:
11   *      http://www.gnu.org/copyleft/lgpl.html
12   * or by writing to:
13   *      Free Software Foundation, Inc.
14   *      59 Temple Place - Suite 330
15   *      Boston, MA 02111-1307, USA
16   *
17   * © CrossWire Bible Society, 2005 - 2016
18   *
19   */
20  package org.crosswire.jsword.passage;
21  
22  import java.io.IOException;
23  import java.io.ObjectInputStream;
24  import java.io.ObjectOutputStream;
25  import java.util.Iterator;
26  
27  import org.crosswire.common.icu.NumberShaper;
28  import org.crosswire.common.util.ItemIterator;
29  import org.crosswire.jsword.JSMsg;
30  import org.crosswire.jsword.JSOtherMsg;
31  import org.crosswire.jsword.versification.BibleBook;
32  import org.crosswire.jsword.versification.BibleNames;
33  import org.crosswire.jsword.versification.Versification;
34  import org.crosswire.jsword.versification.system.Versifications;
35  import org.slf4j.Logger;
36  import org.slf4j.LoggerFactory;
37  
38  /**
39   * A Verse is a pointer to a single verse. Externally its unique identifier is
40   * a String of the form "Gen 1:1" Internally we use
41   * <code>( v11n, book, chapter, verse )</code>
42   * 
43   * <p>
44   * A Verse is designed to be immutable. This is a necessary from a collections
45   * point of view. A Verse should always be valid, although some versions may not
46   * return any text for verses that they consider to be untranslated in some
47   * way.
48   * </p>
49   * 
50   * @see gnu.lgpl.License The GNU Lesser General Public License for details.
51   * @author Joe Walker
52   * @author DM Smith
53   */
54  public final class Verse implements VerseKey<Verse> {
55      /**
56       * Create a Verse from book, chapter and verse numbers, throwing up if the
57       * specified Verse does not exist.
58       * 
59       * @param v11n
60       *            The versification to which this verse belongs
61       * @param book
62       *            The book number (Genesis = 1)
63       * @param chapter
64       *            The chapter number
65       * @param verse
66       *            The verse number
67       */
68      public Verse(Versification v11n, BibleBook book, int chapter, int verse) {
69          this(v11n, book, chapter, verse, null);
70      }
71  
72      /**
73       * Create a Verse from book, chapter and verse numbers, throwing up if the
74       * specified Verse does not exist.
75       * 
76       * @param v11n
77       *            The versification to which this verse belongs
78       * @param book
79       *            The book number (Genesis = 1)
80       * @param chapter
81       *            The chapter number
82       * @param verse
83       *            The verse number
84       * @param subIdentifier
85       *            The optional sub identifier
86       */
87      public Verse(Versification v11n, BibleBook book, int chapter, int verse, String subIdentifier) {
88          this.v11n = v11n;
89          this.book = book;
90          this.chapter = chapter;
91          this.verse = verse;
92          this.subIdentifier = subIdentifier;
93          this.ordinal = v11n.getOrdinal(this);
94      }
95  
96      /**
97       * Create a Verse from book, chapter and verse numbers, patching up if the
98       * specified verse does not exist.
99       * <p>
100      * The actual value of the boolean is ignored. However for future proofing
101      * you should only use 'true'. Do not use patch_up=false, use
102      * <code>Verse(int, int, int)</code> This so that we can declare this
103      * constructor to not throw an exception. Is there a better way of doing
104      * this?
105      * 
106      * @param v11n
107      *            The versification to which this verse belongs
108      * @param book
109      *            The book number (Genesis = 1)
110      * @param chapter
111      *            The chapter number
112      * @param verse
113      *            The verse number
114      * @param patchUp
115      *            True to trigger reference fixing
116      */
117     public Verse(Versification v11n, BibleBook book, int chapter, int verse, boolean patchUp) {
118         if (!patchUp) {
119             throw new IllegalArgumentException(JSOtherMsg.lookupText("Use patchUp=true."));
120         }
121 
122         this.v11n = v11n;
123         Verse patched = this.v11n.patch(book, chapter, verse);
124         this.book = patched.book;
125         this.chapter = patched.chapter;
126         this.verse = patched.verse;
127         this.ordinal = patched.ordinal;
128     }
129 
130     /**
131      * Set a Verse using a verse ordinal number - WARNING Do not use this method
132      * unless you really know the dangers of doing so. Ordinals are not always
133      * going to be the same. So you should use Versification, Book, Chapter and Verse
134      * in preference to an int ordinal whenever possible. Ordinal numbers are 1
135      * based and not 0 based.
136      * 
137      * @param v11n
138      *            The versification to which this verse belongs
139      * @param ordinal
140      *            The verse id
141      */
142     public Verse(Versification v11n, int ordinal) {
143         Verse decoded = v11n.decodeOrdinal(ordinal);
144         this.v11n = v11n;
145         this.book = decoded.book;
146         this.chapter = decoded.chapter;
147         this.verse = decoded.verse;
148         this.ordinal = decoded.ordinal;
149     }
150 
151     /* (non-Javadoc)
152      * @see org.crosswire.jsword.passage.VerseKey#isWhole()
153      */
154     public boolean isWhole() {
155         return subIdentifier == null || subIdentifier.length() == 0;
156     }
157 
158     /* (non-Javadoc)
159      * @see org.crosswire.jsword.passage.VerseKey#getWhole()
160      */
161     public Verse getWhole() {
162         if (isWhole()) {
163             return this;
164         }
165         return new Verse(v11n, book, chapter, verse);
166     }
167 
168     @Override
169     public String toString() {
170         return getName();
171     }
172 
173     /* (non-Javadoc)
174      * @see org.crosswire.jsword.passage.Key#getName()
175      */
176     public String getName() {
177         return getName(null);
178     }
179 
180     /* (non-Javadoc)
181      * @see org.crosswire.jsword.passage.Key#getName(org.crosswire.jsword.passage.Key)
182      */
183     public String getName(Key base) {
184         if (base != null && !(base instanceof Verse)) {
185             return getName();
186         }
187 
188         String verseName = doGetName((Verse) base);
189         // Only shape it if it can be unshaped.
190         if (shaper.canUnshape()) {
191             return shaper.shape(verseName);
192         }
193 
194         return verseName;
195     }
196 
197     /* (non-Javadoc)
198      * @see org.crosswire.jsword.passage.Key#getRootName()
199      */
200     public String getRootName() {
201         return BibleNames.instance().getShortName(book);
202     }
203 
204     /* (non-Javadoc)
205      * @see org.crosswire.jsword.passage.Key#getOsisRef()
206      */
207     public String getOsisRef() {
208         return getOsisID();
209     }
210 
211     /* (non-Javadoc)
212      * @see org.crosswire.jsword.passage.Key#getOsisID()
213      */
214     public String getOsisID() {
215         final StringBuilder buf = getVerseIdentifier();
216         if (subIdentifier != null && subIdentifier.length() > 0) {
217             buf.append(VERSE_OSIS_SUB_PREFIX);
218             buf.append(subIdentifier);
219         }
220         return buf.toString();
221     }
222 
223     /* (non-Javadoc)
224      * @see org.crosswire.jsword.passage.Key#getOsisID()
225      */
226     public String getOsisIDNoSubIdentifier() {
227         return getVerseIdentifier().toString();
228     }
229 
230     /**
231      * Gets the common name of the verse, excluding any !abc sub-identifier
232      * @return the verse OSIS-ID, excluding the sub-identifier
233      */
234     private StringBuilder getVerseIdentifier() {
235         StringBuilder buf = new StringBuilder();
236         buf.append(book.getOSIS());
237         buf.append(Verse.VERSE_OSIS_DELIM);
238         buf.append(chapter);
239         buf.append(Verse.VERSE_OSIS_DELIM);
240         buf.append(verse);
241         return buf;
242     }
243 
244     @Override
245     public Verse clone() {
246         Verse copy = null;
247         try {
248             copy = (Verse) super.clone();
249             copy.v11n = this.v11n;
250             copy.book = this.book;
251             copy.chapter = this.chapter;
252             copy.verse = this.verse;
253             copy.ordinal = this.ordinal;
254             copy.subIdentifier = this.subIdentifier;
255         } catch (CloneNotSupportedException e) {
256             assert false : e;
257         }
258 
259         return copy;
260     }
261 
262     @Override
263     public boolean equals(Object obj) {
264         // Since this can not be null
265         if (!(obj instanceof Verse)) {
266             return false;
267         }
268 
269         Verse that = (Verse) obj;
270 
271         // The real tests
272         return this.ordinal == that.ordinal
273                 && this.v11n.equals(that.v11n)
274                 && bothNullOrEqual(this.subIdentifier, that.subIdentifier);
275     }
276 
277     @Override
278     public int hashCode() {
279         int result = 31 + ordinal;
280         result = 31 * result + ((v11n == null) ? 0 : v11n.hashCode());
281         return 31 * result + ((subIdentifier == null) ? 0 : subIdentifier.hashCode());
282     }
283 
284     /* (non-Javadoc)
285      * @see java.lang.Comparable#compareTo(java.lang.Object)
286      */
287     public int compareTo(Key obj) {
288         return this.ordinal - ((Verse) obj).ordinal;
289     }
290 
291     /* (non-Javadoc)
292      * @see org.crosswire.jsword.passage.VerseKey#getVersification()
293      */
294     public Versification getVersification() {
295         return v11n;
296     }
297 
298     /* (non-Javadoc)
299      * @see org.crosswire.jsword.passage.Passage#reversify(org.crosswire.jsword.versification.Versification)
300      */
301     public Verse reversify(Versification newVersification) {
302         if (v11n.equals(newVersification)) {
303             return this;
304         }
305 
306         try {
307             //check the v11n supports this key, otherwise this leads to all sorts of issues
308             if (newVersification.validate(book, chapter, verse, true)) {
309                 return new Verse(newVersification, book, chapter, verse);
310             }
311         } catch (NoSuchVerseException ex) {
312             // will never happen
313             log.error("Contract for validate was changed to thrown an exception when silent mode is true", ex);
314         }
315         return null;
316     }
317 
318     /**
319      * Return the book that we refer to
320      * 
321      * @return The book of the Bible
322      */
323     public BibleBook getBook() {
324         return book;
325     }
326 
327     /**
328      * Return the chapter that we refer to
329      * 
330      * @return The chapter number
331      */
332     public int getChapter() {
333         return chapter;
334     }
335 
336     /**
337      * Return the verse that we refer to
338      * 
339      * @return The verse number
340      */
341     public int getVerse() {
342         return verse;
343     }
344 
345     /**
346      * Return the sub identifier if any
347      * @return The optional OSIS sub identifier
348      */
349     public String getSubIdentifier() {
350         return subIdentifier;
351     }
352 
353     /**
354      * Return the ordinal value of the verse in its versification.
355      * 
356      * @return The verse number
357      */
358     public int getOrdinal() {
359         return ordinal;
360     }
361 
362     /**
363      * Create an array of Verses
364      * 
365      * @return The array of verses that this makes up
366      */
367     public Verse[] toVerseArray() {
368         return new Verse[] {
369             this
370         };
371     }
372 
373     /* (non-Javadoc)
374      * @see org.crosswire.jsword.passage.Key#getParent()
375      */
376     public Key getParent() {
377         return null;
378     }
379 
380     /**
381      * Determine whether two objects are equal, allowing nulls
382      * @param x
383      * @param y
384      * @return true if both are null or the two are equal
385      */
386     public static boolean bothNullOrEqual(Object x, Object y) {
387         return x == y || (x != null && x.equals(y));
388     }
389 
390     /**
391      * Compute the verse representation given the context.
392      * 
393      * @param verseBase
394      *            the context or null if there is none
395      * @return the verse representation
396      */
397     private String doGetName(Verse verseBase) {
398         StringBuilder buf = new StringBuilder();
399         // To cope with thing like Jude 2...
400         if (book.isShortBook()) {
401             if (verseBase == null || verseBase.book != book) {
402                 buf.append(BibleNames.instance().getPreferredName(book));
403                 buf.append(Verse.VERSE_PREF_DELIM1);
404                 buf.append(verse);
405                 return buf.toString();
406             }
407 
408             return Integer.toString(verse);
409         }
410 
411         if (verseBase == null || verseBase.book != book) {
412             buf.append(BibleNames.instance().getPreferredName(book));
413             buf.append(Verse.VERSE_PREF_DELIM1);
414             buf.append(chapter);
415             buf.append(Verse.VERSE_PREF_DELIM2);
416             buf.append(verse);
417             return buf.toString();
418         }
419 
420         if (verseBase.chapter != chapter) {
421             buf.append(chapter);
422             buf.append(Verse.VERSE_PREF_DELIM2);
423             buf.append(verse);
424             return buf.toString();
425         }
426 
427         return Integer.toString(verse);
428     }
429 
430     /**
431      * This is simply a convenience function to wrap Integer.parseInt() and give
432      * us a reasonable exception on failure. It is called by VerseRange hence
433      * protected, however I would prefer private
434      * 
435      * @param text
436      *            The string to be parsed
437      * @return The correctly parsed chapter or verse
438      */
439     protected static int parseInt(String text) throws NoSuchVerseException {
440         try {
441             return Integer.parseInt(shaper.unshape(text));
442         } catch (NumberFormatException ex) {
443             // TRANSLATOR: The chapter or verse number is actually not a number, but something else.
444             // {0} is a placeholder for what the user supplied.
445             throw new NoSuchVerseException(JSMsg.gettext("Cannot understand {0} as a chapter or verse.", text));
446         }
447     }
448 
449     /**
450      * Write out the object to the given ObjectOutputStream
451      * 
452      * @param out
453      *            The stream to write our state to
454      * @throws IOException
455      *             if the read fails
456      * @serialData Write the ordinal number of this verse
457      */
458     private void writeObject(ObjectOutputStream out) throws IOException {
459         out.defaultWriteObject();
460         out.writeUTF(v11n.getName());
461     }
462 
463     /**
464      * Write out the object to the given ObjectOutputStream
465      * 
466      * @param in
467      *            The stream to read our state from
468      * @throws IOException
469      *             if the read fails
470      * @throws ClassNotFoundException
471      *             If the read data is incorrect
472      * @serialData Write the ordinal number of this verse
473      */
474     private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
475         in.defaultReadObject();
476         String v11nName = in.readUTF();
477         v11n = Versifications.instance().getVersification(v11nName);
478         Verse decoded = v11n.decodeOrdinal(ordinal);
479 
480         this.book = decoded.book;
481         this.chapter = decoded.chapter;
482         this.verse = decoded.verse;
483     }
484 
485     /* (non-Javadoc)
486      * @see org.crosswire.jsword.passage.Key#canHaveChildren()
487      */
488     public boolean canHaveChildren() {
489         return false;
490     }
491 
492     /* (non-Javadoc)
493      * @see org.crosswire.jsword.passage.Key#getChildCount()
494      */
495     public int getChildCount() {
496         return 0;
497     }
498 
499     /* (non-Javadoc)
500      * @see org.crosswire.jsword.passage.Key#getCardinality()
501      */
502     public int getCardinality() {
503         return 1;
504     }
505 
506     /* (non-Javadoc)
507      * @see org.crosswire.jsword.passage.Key#isEmpty()
508      */
509     public boolean isEmpty() {
510         return false;
511     }
512 
513     /* (non-Javadoc)
514      * @see org.crosswire.jsword.passage.Key#contains(org.crosswire.jsword.passage.Key)
515      */
516     public boolean contains(Key key) {
517         return this.equals(key);
518     }
519 
520     /* (non-Javadoc)
521      * @see java.lang.Iterable#iterator()
522      */
523     public Iterator<Key> iterator() {
524         return new ItemIterator<Key>(this);
525     }
526 
527     /* (non-Javadoc)
528      * @see org.crosswire.jsword.passage.Key#addAll(org.crosswire.jsword.passage.Key)
529      */
530     public void addAll(Key key) {
531         throw new UnsupportedOperationException();
532     }
533 
534     /* (non-Javadoc)
535      * @see org.crosswire.jsword.passage.Key#removeAll(org.crosswire.jsword.passage.Key)
536      */
537     public void removeAll(Key key) {
538         throw new UnsupportedOperationException();
539     }
540 
541     /* (non-Javadoc)
542      * @see org.crosswire.jsword.passage.Key#retainAll(org.crosswire.jsword.passage.Key)
543      */
544     public void retainAll(Key key) {
545         throw new UnsupportedOperationException();
546     }
547 
548     /* (non-Javadoc)
549      * @see org.crosswire.jsword.passage.Key#clear()
550      */
551     public void clear() {
552         // do nothing
553     }
554 
555     /* (non-Javadoc)
556      * @see org.crosswire.jsword.passage.Key#get(int)
557      */
558     public Key get(int index) {
559         if (index == 0) {
560             return this;
561         }
562         return null;
563     }
564 
565     /* (non-Javadoc)
566      * @see org.crosswire.jsword.passage.Key#indexOf(org.crosswire.jsword.passage.Key)
567      */
568     public int indexOf(Key that) {
569         if (this.equals(that)) {
570             return 0;
571         }
572         return -1;
573     }
574 
575     /* (non-Javadoc)
576      * @see org.crosswire.jsword.passage.Key#blur(int, org.crosswire.jsword.passage.RestrictionType)
577      */
578     public void blur(int by, RestrictionType restrict) {
579         throw new UnsupportedOperationException();
580     }
581 
582     /**
583      * What characters should we use to separate parts of an OSIS verse
584      * reference
585      */
586     public static final char VERSE_OSIS_DELIM = '.';
587 
588     /**
589      * What characters should we use to start an OSIS sub identifier
590      */
591     public static final char VERSE_OSIS_SUB_PREFIX = '!';
592 
593     /**
594      * What characters should we use to separate the book from the chapter
595      */
596     public static final char VERSE_PREF_DELIM1 = ' ';
597 
598     /**
599      * What characters should we use to separate the chapter from the verse
600      */
601     public static final char VERSE_PREF_DELIM2 = ':';
602 
603     /**
604      * The default verse
605      */
606     public static final Verse DEFAULT = new Verse(Versifications.instance().getVersification("KJV"), BibleBook.GEN, 1, 1);
607 
608     /**
609      * Allow the conversion to and from other number representations.
610      */
611     private static NumberShaper shaper = new NumberShaper();
612 
613     /**
614      * The versification for this verse.
615      */
616     private transient Versification v11n;
617 
618     /**
619      * The ordinal value for this verse within its versification.
620      */
621     private int ordinal;
622 
623     /**
624      * The book of the Bible.
625      */
626     private transient BibleBook book;
627 
628     /**
629      * The chapter number
630      */
631     private transient int chapter;
632 
633     /**
634      * The verse number
635      */
636     private transient int verse;
637 
638     /**
639      * The OSIS Sub-identifier if present.
640      * This should be a string that allows for the likes of:
641      * a.xy.asdf.qr
642      */
643     private String subIdentifier;
644 
645     private static final Logger log = LoggerFactory.getLogger(Verse.class);
646 
647     /**
648      * To make serialization work across new versions
649      */
650     private static final long serialVersionUID = -4033921076023185171L;
651 }
652