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, 2013 - 2016
18   *
19   */
20  package org.crosswire.jsword.versification;
21  
22  import org.crosswire.jsword.passage.Verse;
23  import org.crosswire.jsword.passage.VerseKey;
24  import org.crosswire.jsword.passage.VerseRange;
25  import org.crosswire.jsword.versification.system.Versifications;
26  
27  /**
28   * A QualifiedKey represents the various left and right sides of a map entry.
29   * <p>
30   * The QualifiedKey is Qualified:
31   * </p>
32   * <ul>
33   * <li><strong>DEFAULT</strong> - This QualifiedKey is either a Verse or a VerseRange.</li>
34   * <li><strong>ABSENT_IN_KJV</strong> - This QualifiedKey has a section name for what is absent in the KJV (the right hand of the map entry).</li>
35   * <li><strong>ABSENT_IN_LEFT</strong> - This QualifiedKey has no other content.</li>
36   * </ul>
37   * <p>
38   * The mapping can indicate a part of a verse. This is an internal implementation detail of the Versification mapping code.
39   * Here it is used to distinguish one QualifiedKey from another in equality tests and in containers.
40   * </p>
41   *
42   * @author Chris Burrell
43   * @see gnu.lgpl.License The GNU Lesser General Public License for details.<br>
44   * The copyright to this program is held by its authors.
45   */
46  public final class QualifiedKey {
47      /**
48       * A Qualifier indicates whether the verse is numbered the same in both the KJV and the other, is missing in the KJV or the other.
49       */
50      enum Qualifier {
51          /**
52           * The DEFAULT Qualifier indicates a Verse or a VerseRange.
53           */
54          DEFAULT {
55              @Override
56              public String getDescription(QualifiedKey q) {
57                  return "";
58              }
59          },
60          /**
61           * The ABSENT_IN_LEFT Qualifier indicates that the left side of the map has no equivalent on the right (KJV).
62           */
63          ABSENT_IN_LEFT {
64              @Override
65              public String getDescription(QualifiedKey q) {
66                  return "Absent in Left";
67              }
68          },
69          /**
70           * The ABSENT_IN_KJV Qualifier indicates that the right (KJV) side of the map has no equivalent on the left.
71           */
72          ABSENT_IN_KJV {
73              @Override
74              public String getDescription(QualifiedKey q) {
75                  return q != null && q.getSectionName() != null ? q.getSectionName() : "Missing section name";
76              }
77          };
78  
79          /**
80           * @param q the QualifiedKey that this describes
81           * @return The description for the qualified key
82           */
83          public abstract String getDescription(QualifiedKey q);
84  
85      }
86  
87      /**
88       * Construct a QualifiedKey from a Verse.
89       *
90       * @param key the verse from which to create this QualifiedKey
91       */
92      protected QualifiedKey(Verse key) {
93          setKey(key);
94          this.absentType = Qualifier.DEFAULT;
95      }
96  
97      /**
98       * Construct a QualifiedKey from a Verse.
99       *
100      * @param key the verse range from which to create this QualifiedKey
101      */
102     public QualifiedKey(VerseRange key) {
103         setKey(key);
104         this.absentType = Qualifier.DEFAULT;
105     }
106 
107     /**
108      * @param sectionName with a given section name, we assume absent in KJV
109      */
110     public QualifiedKey(String sectionName) {
111         this.sectionName = sectionName;
112         this.absentType = Qualifier.ABSENT_IN_KJV;
113     }
114 
115     /**
116      * Constructs the QualifiedKey with the ABSENT_IN_LEFT qualifier.
117      * This really means that there are no fields in this QualifiedKey.
118      */
119     public QualifiedKey() {
120         this.absentType = Qualifier.ABSENT_IN_LEFT;
121     }
122 
123     /**
124      * Create a QualifiedKey from a Verse or a VerseRange.
125      *
126      * @param k the Verse or VerseRange
127      * @return the created QualifiedKey
128      * @throws ClassCastException
129      */
130     public static QualifiedKey create(VerseKey k) {
131         return k instanceof Verse ? new QualifiedKey((Verse) k) : new QualifiedKey((VerseRange) k);
132     }
133 
134     /**
135      * @return * The internal key which is either a Verse or VerseRange
136      */
137     public VerseKey getKey() {
138         return wholeKey;
139     }
140 
141     /**
142      * @return * The internal key cast as a Verse
143      * @throws ClassCastException
144      */
145     public Verse getVerse() {
146         return (Verse) wholeKey;
147     }
148 
149     /**
150      * @return the type of the unknown qualifier
151      */
152     public Qualifier getAbsentType() {
153         return absentType;
154     }
155 
156     /**
157      * @return the name (any name) of the section represented within the KJV
158      */
159     public String getSectionName() {
160         return sectionName;
161     }
162 
163     /**
164      * A QualifiedKey is whole if it does not split part of a reference.
165      *
166      * @return whether this QualifiedKey has a whole reference
167      */
168     public boolean isWhole() {
169         // If the reference is null, then it cannot be part or whole.
170         // But we say it is whole because the calls to this are really testing
171         // to see if it is a part.
172         return qualifiedKey == null || qualifiedKey.isWhole();
173     }
174 
175     /**
176      * Convert this QualifiedKey from one Versification to another.
177      * This is a potentially dangerous operation that does no mapping
178      * from one versification to another. Use it only when it is known
179      * to be safe.
180      *
181      * @param target The target versification
182      * @return The reversified QualifiedKey
183      */
184     public QualifiedKey reversify(Versification target) {
185         // Only if it has a qualified key can it be reversified
186         if (this.qualifiedKey == null) {
187             return this;
188         }
189 
190         final VerseKey reversifiedKey = qualifiedKey.reversify(target);
191         if (reversifiedKey != null) {
192             return create(reversifiedKey);
193         }
194 
195         if (target.getName().equals(Versifications.DEFAULT_V11N)) {
196             //then we're absent in KJV
197             return new QualifiedKey(qualifiedKey.getOsisID());
198         }
199         return new QualifiedKey();
200 
201     }
202 
203     @Override
204     public String toString() {
205         StringBuilder buf = new StringBuilder();
206         if (wholeKey != null) {
207             buf.append(qualifiedKey.getOsisRef());
208         }
209         String desc = absentType.getDescription(this);
210         if (desc.length() > 0) {
211             if (buf.length() > 0) {
212                 buf.append(": ");
213             }
214             buf.append(absentType.getDescription(this));
215         }
216         return buf.toString();
217     }
218 
219     @Override
220     public int hashCode() {
221         // Use a prime number in case one of the values is not around
222         return (this.qualifiedKey == null ? 17 : qualifiedKey.hashCode())
223                 + (this.absentType == null ? 13 : this.absentType.ordinal())
224                 + (this.sectionName == null ? 19 : this.sectionName.hashCode());
225     }
226 
227     @Override
228     public boolean equals(final Object obj) {
229         if (obj instanceof QualifiedKey) {
230             final QualifiedKey otherKey = (QualifiedKey) obj;
231             return this.getAbsentType() == otherKey.getAbsentType()
232                     && bothNullOrEqual(this.sectionName, otherKey.sectionName)
233                     && bothNullOrEqual(this.qualifiedKey, otherKey.qualifiedKey);
234         }
235         return false;
236     }
237 
238     /**
239      * Allow override of the key, particular useful if we're constructing in 2 stages like the offset mechanism
240      *
241      * @param key the new key
242      */
243     private void setKey(final Verse key) {
244         this.qualifiedKey = key;
245         this.wholeKey = key.getWhole();
246     }
247 
248     /**
249      * Allow override of the key, particular useful if we're constructing in 2 stages like the offset mechanism
250      *
251      * @param key the new key
252      */
253     private void setKey(final VerseRange key) {
254         if (key.getCardinality() == 1) {
255             this.qualifiedKey = key.getStart();
256         } else {
257             this.qualifiedKey = key;
258         }
259         this.wholeKey = this.qualifiedKey.getWhole();
260     }
261 
262     /**
263      * Determine whether two objects are equal, allowing nulls
264      *
265      * @param x
266      * @param y
267      * @return true if both are null or the two are equal
268      */
269     private static boolean bothNullOrEqual(Object x, Object y) {
270         return x == y || (x != null && x.equals(y));
271     }
272 
273     private VerseKey qualifiedKey;
274     private VerseKey wholeKey;
275     private String sectionName;
276     private Qualifier absentType;
277 
278 }
279