Coverage Report - org.crosswire.jsword.versification.VersificationToKJVMapper
 
Classes in this File Line Coverage Branch Coverage Complexity
VersificationToKJVMapper
0%
0/196
0%
0/115
4.217
 
 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 java.io.ByteArrayOutputStream;
 23  
 import java.io.PrintStream;
 24  
 import java.io.UnsupportedEncodingException;
 25  
 import java.util.ArrayList;
 26  
 import java.util.HashMap;
 27  
 import java.util.Iterator;
 28  
 import java.util.List;
 29  
 import java.util.Map;
 30  
 
 31  
 import org.crosswire.common.util.IOUtil;
 32  
 import org.crosswire.common.util.KeyValuePair;
 33  
 import org.crosswire.common.util.LucidRuntimeException;
 34  
 import org.crosswire.jsword.JSMsg;
 35  
 import org.crosswire.jsword.passage.Key;
 36  
 import org.crosswire.jsword.passage.KeyUtil;
 37  
 import org.crosswire.jsword.passage.NoSuchKeyException;
 38  
 import org.crosswire.jsword.passage.NoSuchVerseException;
 39  
 import org.crosswire.jsword.passage.OsisParser;
 40  
 import org.crosswire.jsword.passage.Passage;
 41  
 import org.crosswire.jsword.passage.RangedPassage;
 42  
 import org.crosswire.jsword.passage.RestrictionType;
 43  
 import org.crosswire.jsword.passage.Verse;
 44  
 import org.crosswire.jsword.passage.VerseKey;
 45  
 import org.crosswire.jsword.passage.VerseRange;
 46  
 import org.crosswire.jsword.versification.system.Versifications;
 47  
 import org.slf4j.Logger;
 48  
 import org.slf4j.LoggerFactory;
 49  
 
 50  
 /**
 51  
  * A Versification mapper allows you to a map a given verse to the KJV versification,
 52  
  * or unmap it from the KJV versification into your own versification.
 53  
  * <p>
 54  
  * A properties-like file will contain the non-KJV versification as they key, and the KJV versification value
 55  
  * as the target value... Duplicate keys are allowed.
 56  
  * </p>
 57  
  * <p>
 58  
  * i.e. Gen.1.1=Gen.1.2 means Gen.1.1 in X versification is Gen.1.2 in the KJV versification
 59  
  * </p>
 60  
  * <p>
 61  
  * You can specify a range on either side. If a range is present on both sides, they have to have the same number of
 62  
  * verses, i.e. verses are mapped verse by verse to each other<br>
 63  
  * Gen.1.1-Gen.1.2=Gen.1.2-Gen.1.3 means Gen.1.1=Gen.1.2 and Gen.1.2=Gen.1.3<br>
 64  
  *<br>
 65  
  * Note: if the cardinality of the left &amp; KJV sides are different by only one, the algorithm makes the
 66  
  * assumption that verse 0 should be disregarded in both ranges.
 67  
  * </p>
 68  
  * <p>
 69  
  * Mappings can be specified by offset. In this case, be aware this maps verse 0 as well. So for example:
 70  
  * </p>
 71  
  * <p>
 72  
  * Ps.19-Ps.20=-1 means Ps.19.0=Ps.18.50, Ps.19.1=Ps.19.0, Ps.19.2=Ps.19.1, etc.<br>
 73  
  * It does not make much sense to have an offset for a single verse, so this is not supported.
 74  
  * Offsetting for multiple ranges however does, and operates range by range, i.e. each range is calculated separately.
 75  
  * Offsetting is somewhat equivalent to specifying ranges, and as a result, the verse 0 behaviour is identical.
 76  
  * </p>
 77  
  * <p>
 78  
  * You can use part-mappings. This is important if you want to preserve transformations from one side to another without
 79  
  * losing resolution of the verse.
 80  
  * </p>
 81  
  * <p>
 82  
  * For example,<br>
 83  
  * if V1 defines Gen.1.1=Gen.1.1, Gen.1.2=Gen1.1<br>
 84  
  * if V2 defines Gen.1.1=Gen.1.1, Gen.1.2=Gen.1.1<br>
 85  
  * then, mapping from V1=&gt;KJV and KJV=&gt;V2 gives you Gen.1.1=&gt;Gen.1.1=&gt;Gen.1.1-Gen.1.2 which is inaccurate if in fact
 86  
  * V1(Gen.1.1) actually equals V2(Gen.1.1). So instead, we use a split on the right hand-side:
 87  
  * </p>
 88  
  * <p>
 89  
  * For example,<br>
 90  
  * V1 defines Gen.1.1=Gen1.1!a, Gen.1.2=Gen.1.1!b<br>
 91  
  * V2 defines Gen.1.1=Gen1.1!a, Gen.1.2=Gen.1.1!b<br>
 92  
  * then, mapping from V1=&gt;KJV and KJV=&gt;V2 gives you Gen.1.1=&gt;Gen.1.1!a=&gt;Gen.1.1, which is now accurate.
 93  
  * A part is a string fragment placed after the end of a key reference. We cannot use # because that is commonly known
 94  
  * as a comment in real properties-file. Using a marker, means we can have meaningful part names if we so choose.
 95  
  * Parts of ranges are not supported.
 96  
  * </p>
 97  
  * <p>
 98  
  * Note: splits should never be seen by a user. The mapping from one versification to another is abstracted
 99  
  * such that the user can simply request the mapping between 2 verse (ranges).
 100  
  * </p>
 101  
  * <p>
 102  
  * Unmapped verses can be specified by inventing ids, either for whole sections, or verse by verse (this would
 103  
  * come in handy if two versifications have the same content, but the KJV doesn't). A section must be preceded
 104  
  * with a '?' character indicating that there will be no need to try and look up a reference.
 105  
  * Gen.1.1=?NewPassage
 106  
  * </p>
 107  
  * <p>
 108  
  * Since not specifying a verse mappings means there is a 1-2-1 unchanged mapping, we need a way of specifying
 109  
  * absent verses altogether:<br>
 110  
  * ?=Gen.1.1<br>
 111  
  * ?=Gen.1.5<br>
 112  
  * means that the non-KJV book simply does not contain verses Gen.1.1 and Gen.1.5 and therefore can't
 113  
  * be mapped.
 114  
  * </p>
 115  
  * <p>
 116  
  * We allow some global flags (one at present):<br>
 117  
  * !zerosUnmapped : means that any mapping to or from a zero verse
 118  
  * </p>
 119  
  * <p>
 120  
  * TODO(CJB): think about whether when returning, we should clone, or make things immutable.
 121  
  * </p>
 122  
  * 
 123  
  * @see gnu.lgpl.License The GNU Lesser General Public License for details.
 124  
  * @author Chris Burrell
 125  
  */
 126  
 public class VersificationToKJVMapper {
 127  
 
 128  
     /**
 129  
      * @param nonKjv a versification that is not the KJV
 130  
      * @param mapping the mappings from one versification to another
 131  
      */
 132  0
     public VersificationToKJVMapper(Versification nonKjv, final FileVersificationMapping mapping) {
 133  0
         absentVerses = createEmptyPassage(KJV);
 134  0
         toKJVMappings = new HashMap<VerseKey, List<QualifiedKey>>();
 135  0
         fromKJVMappings = new HashMap<QualifiedKey, Passage>();
 136  0
         this.nonKjv = nonKjv;
 137  0
         processMappings(mapping);
 138  0
         trace();
 139  0
     }
 140  
 
 141  
     /**
 142  
      * This is the crux of the decoding facility.  The properties are expanded.
 143  
      *
 144  
      * @param mappings the input mappings, in a contracted, short-hand form
 145  
      */
 146  
     private void processMappings(FileVersificationMapping mappings) {
 147  0
         final List<KeyValuePair> entries = mappings.getMappings();
 148  0
         for (KeyValuePair entry : entries) {
 149  
             try {
 150  0
                 processEntry(entry);
 151  0
             } catch (NoSuchKeyException ex) {
 152  
                 // TODO(CJB): should we throw a config exception?
 153  0
                 LOGGER.error("Unable to process entry [{}] with value [{}]", entry.getKey(), entry.getValue(), ex);
 154  0
                 hasErrors = true;
 155  0
             }
 156  
         }
 157  0
     }
 158  
 
 159  
     private void processEntry(final KeyValuePair entry) throws NoSuchKeyException {
 160  0
         String leftHand = entry.getKey();
 161  0
         String kjvHand = entry.getValue();
 162  
 
 163  0
         if (leftHand == null || leftHand.length() == 0) {
 164  0
             LOGGER.error("Left hand must have content");
 165  0
             return;
 166  
         }
 167  
 
 168  
         // we allow some global flags properties - for a want of a better syntax!
 169  
         // TODO(DMS): remove this from the mapping files
 170  0
         if ("!zerosUnmapped".equals(leftHand)) {
 171  0
             return;
 172  
         }
 173  
 
 174  
         // At this point, the left hand side must be a Verse or a VerseRange
 175  
         // It cannot be prefixed by +, ?, or !.
 176  0
         QualifiedKey left = getRange(this.nonKjv, leftHand, null);
 177  
 
 178  
         // The right hand side can start with ? which means that the left maps to nothing in the KJV.
 179  
         // The ? leads a section name
 180  0
         QualifiedKey kjv = getRange(KJV, kjvHand, left.getKey());
 181  0
         addMappings(left, kjv);
 182  0
     }
 183  
 
 184  
     /**
 185  
      * Adds a 1-Many mapping, by simply listing out all the properties. There is probably
 186  
      * a better way for storing this, perhaps in a tree - but for simplicity, we're going to list them out.
 187  
      *
 188  
      * @param leftHand  the left hand side operand
 189  
      * @param kjvVerses the verses that are mapped by the left hand side
 190  
      */
 191  
     private void addMappings(final QualifiedKey leftHand, final QualifiedKey kjvVerses) throws NoSuchVerseException {
 192  0
         if (leftHand.getAbsentType() == QualifiedKey.Qualifier.ABSENT_IN_LEFT) {
 193  0
             this.absentVerses.addAll(kjvVerses.getKey());
 194  0
         } else if (leftHand.getKey().getCardinality() == 1) {
 195  0
             add1ToManyMappings(leftHand.getVerse(), kjvVerses);
 196  
         } else {
 197  0
             addManyToMany(leftHand, kjvVerses);
 198  
         }
 199  0
     }
 200  
 
 201  
     /**
 202  
      * Adds a many to many mapping, mappings all the verses on the left hand side to all the verses on the right hand side.
 203  
      * We support 2 types: Many-to-1 and Many-to-Many.
 204  
      *
 205  
      * @param leftHand is assumed to be many
 206  
      * @param kjvVerses could be 1 or many
 207  
      */
 208  
     private void addManyToMany(final QualifiedKey leftHand, final QualifiedKey kjvVerses) {
 209  0
         VerseKey leftKeys = leftHand.getKey();
 210  0
         VerseKey kjvKeys = kjvVerses.getKey();
 211  0
         Iterator<Key> leftIter = leftKeys.iterator();
 212  
 
 213  0
         if (kjvKeys != null && kjvKeys.getCardinality() != 1) {
 214  
             // We detect if the keys are 1-apart from each other. If so, then we skip verse 0 on both sides.
 215  0
             int diff = Math.abs(leftKeys.getCardinality() - kjvKeys.getCardinality());
 216  
 
 217  0
             if (diff > 1) {
 218  0
                 reportCardinalityError(leftKeys, kjvKeys);
 219  
             }
 220  0
             boolean skipVerse0 = diff == 1;
 221  
 
 222  0
             Iterator<Key> kjvIter = kjvKeys.iterator();
 223  0
             while (leftIter.hasNext()) {
 224  0
                 Verse leftVerse = (Verse) leftIter.next();
 225  
 
 226  
                 // hasNext() and next() have to be paired
 227  0
                 if (!kjvIter.hasNext()) {
 228  0
                     reportCardinalityError(leftKeys, kjvKeys);
 229  
                 }
 230  
 
 231  0
                 Verse rightVerse = (Verse) kjvIter.next();
 232  0
                 QualifiedKey kjvKey = new QualifiedKey(rightVerse);
 233  
 
 234  
                 // When the lists are of lengths differing by one
 235  
                 // it is because 0 is extra.
 236  
                 // When this is encountered on the left side,
 237  
                 // we map it and the next verse to the current
 238  
                 // right verse.
 239  
                 // In this block we'll handle the case of mapping 0
 240  
                 // and at the end of the loop we'll handle the next verse.
 241  0
                 if (skipVerse0 && leftVerse.getVerse() == 0) {
 242  
 
 243  0
                     addForwardMappingFromSingleKeyToRange(leftVerse, kjvKey);
 244  0
                     addKJVToMapping(kjvKey, leftVerse);
 245  
 
 246  0
                     if (!leftIter.hasNext()) {
 247  0
                         reportCardinalityError(leftKeys, kjvKeys);
 248  
                     }
 249  
 
 250  0
                     leftVerse = (Verse) leftIter.next();
 251  
                 }
 252  
 
 253  
                 // Likewise for the right side,
 254  
                 // we map it and the next verse to the current
 255  
                 // left verse.
 256  
                 // Likewise for this block, it only handles mapping 0
 257  
                 // Code at the end will map the following verse
 258  0
                 if (skipVerse0 && rightVerse.getVerse() == 0) {
 259  
 
 260  0
                     addForwardMappingFromSingleKeyToRange(leftVerse, kjvKey);
 261  0
                     addKJVToMapping(kjvKey, leftVerse);
 262  
 
 263  0
                     if (!kjvIter.hasNext()) {
 264  0
                         reportCardinalityError(leftKeys, kjvKeys);
 265  
                     }
 266  
 
 267  0
                     rightVerse = (Verse) kjvIter.next();
 268  0
                     kjvKey = new QualifiedKey(rightVerse);
 269  
                 }
 270  
 
 271  
                 // Now do the normal case mapping
 272  0
                 addForwardMappingFromSingleKeyToRange(leftVerse, kjvKey);
 273  0
                 addKJVToMapping(kjvKey, leftVerse);
 274  0
             }
 275  
 
 276  
             // Check to see if, having exhausted left that there is more
 277  
             // on the right
 278  0
             if (kjvIter.hasNext()) {
 279  0
                 reportCardinalityError(leftKeys, kjvKeys);
 280  
             }
 281  0
         } else {
 282  0
             while (leftIter.hasNext()) {
 283  0
                 final Verse leftKey = (Verse) leftIter.next();
 284  0
                 addForwardMappingFromSingleKeyToRange(leftKey, kjvVerses);
 285  0
                 addKJVToMapping(kjvVerses, leftKey);
 286  0
             }
 287  
         }
 288  
 
 289  0
     }
 290  
 
 291  
     /**
 292  
      * If for some reason cardinalities of keys are different, we report it.
 293  
      *
 294  
      * @param leftKeys  the left hand key
 295  
      * @param kjvKeys the kjv qualified key
 296  
      */
 297  
     private void reportCardinalityError(final VerseKey leftKeys, final VerseKey kjvKeys) {
 298  
         // TODO (CJB): change this to a neater exception
 299  
         // then something went wrong, as we have remaining verses
 300  0
         throw new LucidRuntimeException(String.format("%s has a cardinality of %s whilst %s has a cardinality of %s.",
 301  
                 leftKeys, Integer.toString(leftKeys.getCardinality()),
 302  
                 kjvKeys, Integer.toString(kjvKeys.getCardinality())));
 303  
     }
 304  
 
 305  
     /**
 306  
      * If leftKey is non-null (i.e. not attached to a simple specifier, then adds to the kjvTo mappings
 307  
      *
 308  
      * @param kjvVerses the kjv verses
 309  
      * @param leftKey   the left-hand key, possibly null.
 310  
      */
 311  
     private void addKJVToMapping(final QualifiedKey kjvVerses, final Verse leftKey) {
 312  
         // NOTE(DMS): Both kjvVerses and left key are each a single verse
 313  0
         if (leftKey != null) {
 314  0
             getNonEmptyKey(this.fromKJVMappings, kjvVerses).addAll(leftKey);
 315  
 
 316  
             // If we have a part, then we need to add the whole verse as well...
 317  0
             if (!kjvVerses.isWhole()) {
 318  0
                 getNonEmptyKey(this.fromKJVMappings, QualifiedKey.create(kjvVerses.getKey().getWhole())).addAll(leftKey);
 319  
             }
 320  
         }
 321  0
     }
 322  
 
 323  
     /**
 324  
      * A simple two way entry between 2 1-1 entries.
 325  
      *
 326  
      * @param leftHand the verse on the left side, left is assumed to be 1 verse only
 327  
      * @param kjvHand  the KJV reference
 328  
      * @throws NoSuchVerseException
 329  
      */
 330  
     private void add1ToManyMappings(final Verse leftHand, final QualifiedKey kjvHand) throws NoSuchVerseException {
 331  0
         addForwardMappingFromSingleKeyToRange(leftHand, kjvHand);
 332  0
         addReverse1ToManyMappings(leftHand, kjvHand);
 333  0
     }
 334  
 
 335  
     /**
 336  
      * Adds the data into the reverse mappings. Caters for 1-2-1 and 1-2-Many mappings
 337  
      *
 338  
      * @param leftHand the reference of the left hand reference
 339  
      * @param kjvHand  the kjv reference/key, qualified with the part
 340  
      */
 341  
     private void addReverse1ToManyMappings(final Verse leftHand, final QualifiedKey kjvHand) {
 342  
         //add the reverse mapping, for 1-1 mappings
 343  0
         if (kjvHand.getAbsentType() == QualifiedKey.Qualifier.ABSENT_IN_KJV || kjvHand.getKey().getCardinality() == 1) {
 344  
             // TODO(CJB): deal with parts
 345  0
             addKJVToMapping(kjvHand, leftHand);
 346  
         } else {
 347  
             //add the 1-many mappings
 348  
             //expand the key and add them all
 349  
             //Parts are not supported on ranges...
 350  0
             Iterator<Key> kjvKeys = kjvHand.getKey().iterator();
 351  0
             while (kjvKeys.hasNext()) {
 352  0
                 addKJVToMapping(new QualifiedKey(KeyUtil.getVerse(kjvKeys.next())), leftHand);
 353  
             }
 354  
         }
 355  0
     }
 356  
 
 357  
     /**
 358  
      * Adds a forward mappings from left to KJV.
 359  
      *
 360  
      * @param leftHand the left hand reference (corresponding to a non-kjv versification)
 361  
      * @param kjvHand  the kjv reference (with part if applicable).
 362  
      */
 363  
     private void addForwardMappingFromSingleKeyToRange(final Verse leftHand, final QualifiedKey kjvHand) {
 364  0
         if (leftHand == null) {
 365  0
             return;
 366  
         }
 367  
 
 368  0
         getNonEmptyMappings(this.toKJVMappings, leftHand).add(kjvHand);
 369  0
     }
 370  
 
 371  
     /**
 372  
      * Gets a non-empty key list, either new or the one existing in the map already.
 373  
      *
 374  
      * @param mappings the map from key to list of values
 375  
      * @param key      the key
 376  
      * @return the non-empty mappings list
 377  
      */
 378  
     private VerseKey getNonEmptyKey(final Map<QualifiedKey, Passage> mappings, final QualifiedKey key) {
 379  0
         Passage matchingVerses = mappings.get(key);
 380  0
         if (matchingVerses == null) {
 381  0
             matchingVerses = createEmptyPassage(this.nonKjv);
 382  0
             mappings.put(key, matchingVerses);
 383  
         }
 384  0
         return matchingVerses;
 385  
     }
 386  
 
 387  
     /**
 388  
      * Gets a non-empty list, either new or the one existing in the map already
 389  
      *
 390  
      * @param mappings the map from key to list of values
 391  
      * @param key      the key
 392  
      * @param <T>      the type of the key
 393  
      * @param <S>      the type of the value
 394  
      * @return the separate list of verses
 395  
      */
 396  
     private <T, S> List<S> getNonEmptyMappings(final Map<T, List<S>> mappings, final T key) {
 397  0
         List<S> matchingVerses = mappings.get(key);
 398  0
         if (matchingVerses == null) {
 399  0
             matchingVerses = new ArrayList<S>();
 400  0
             mappings.put(key, matchingVerses);
 401  
         }
 402  0
         return matchingVerses;
 403  
     }
 404  
 
 405  
     /**
 406  
      * Expands a reference to all its verses
 407  
      *
 408  
      * @param versesKey the verses
 409  
      * @return the separate list of verses
 410  
      */
 411  
     private QualifiedKey getRange(final Versification versification, String versesKey, VerseKey offsetBasis) throws NoSuchKeyException {
 412  
         //deal with absent keys in left & absent keys in right, which are simply marked by a '?'
 413  0
         if (versesKey == null || versesKey.length() == 0) {
 414  0
             throw new NoSuchKeyException(JSMsg.gettext("Cannot understand [{0}] as a chapter or verse.", versesKey));
 415  
         }
 416  
 
 417  0
         char firstChar = versesKey.charAt(0);
 418  0
         switch (firstChar) {
 419  
             case '?':
 420  
                 // TODO(CJB): The class JavaDoc has ? on the left side
 421  
                 // Where is that in any of the mapping code, other than in tests?
 422  
                 // NOTE(DMS): Does not return a range of any kind.
 423  0
                 return getAbsentQualifiedKey(versification, versesKey);
 424  
             case '+':
 425  
             case '-':
 426  
                 // TODO(CJB): Is + or - allowed only on the right hand side
 427  
                 // NOTE(DMS): Always returns a QualifiedKey containing a Passage having a VerseRange of 1 or more verses
 428  0
                 return getOffsetQualifiedKey(versification, versesKey, offsetBasis);
 429  
             default:
 430  0
                 return getExistingQualifiedKey(versification, versesKey);
 431  
         }
 432  
     }
 433  
 
 434  
     /**
 435  
      * Deals with offset markers, indicating a passage is +x or -x verses from this one.
 436  
      *
 437  
      * @param versification the versification of the passed in key
 438  
      * @param versesKey     the text of the reference we are trying to parse
 439  
      * @return the qualified key representing this
 440  
      */
 441  
     private QualifiedKey getOffsetQualifiedKey(final Versification versification, final String versesKey, VerseKey offsetBasis) throws NoSuchKeyException {
 442  0
         if (offsetBasis == null || offsetBasis.getCardinality() == 0) {
 443  
             // TODO(CJB): internationalize
 444  0
             throw new NoSuchKeyException(JSMsg.gettext("Unable to offset the given key [{0}]", offsetBasis));
 445  
         }
 446  0
         int offset = Integer.parseInt(versesKey.substring(1));
 447  
 
 448  
         // Convert key immediately to the target versification system, namely the KJV, since it is the only
 449  
         // one supported. Convert by ref - since the whole purpose of this is to define equivalents.
 450  
 
 451  0
          VerseRange vr = null;
 452  0
          if (offsetBasis instanceof VerseRange) {
 453  0
              vr = (VerseRange) offsetBasis;
 454  0
          } else if (offsetBasis instanceof Passage) {
 455  0
              Iterator iter = ((Passage) offsetBasis).rangeIterator(RestrictionType.NONE);
 456  0
              if (iter.hasNext()) {
 457  0
                  vr = (VerseRange) iter.next();
 458  
              }
 459  
          }
 460  0
          if (vr == null) {
 461  
              // TODO(CJB): internationalize
 462  0
              throw new NoSuchKeyException(JSMsg.gettext("Unable to offset the given key [{0}]", offsetBasis));
 463  
          }
 464  
 
 465  0
          Verse vrStart = vr.getStart();
 466  0
          Verse start = vrStart.reversify(versification);
 467  
          // While you can add a negative number, these are optimized for their operation
 468  0
          if (offset < 0) {
 469  0
              start = versification.subtract(start, -offset);
 470  0
          } else if (offset > 0) {
 471  0
              start = versification.add(start, offset);
 472  
          }
 473  0
          Verse end = start;
 474  0
          if (vr.getCardinality() > 1) {
 475  0
              end = versification.add(start, vr.getCardinality() - 1);
 476  
          }
 477  
 
 478  0
         if (start == null || end == null) {
 479  0
             hasErrors = true;
 480  0
             LOGGER.error("Verse range with offset did not map to correct range in target versification. This mapping will be set to an empty unmapped key.");
 481  
         }
 482  
 
 483  0
          return start != null && end != null ? new QualifiedKey(new VerseRange(versification, start, end)) : new QualifiedKey(versesKey);
 484  
     }
 485  
 
 486  
     /**
 487  
      * Deals with real keys found in the versification.
 488  
      *
 489  
      * @param versification the versification of the passed in key
 490  
      * @param versesKey     the text of the reference we are trying to parse
 491  
      * @return the qualified key representing this
 492  
      */
 493  
     private QualifiedKey getExistingQualifiedKey(final Versification versification, final String versesKey) {
 494  0
         return new QualifiedKey(osisParser.parseOsisRef(versification, versesKey));
 495  
     }
 496  
 
 497  
     /**
 498  
      * Deals with absent markers, whether absent in the KJV or absent in the current versification.
 499  
      *
 500  
      * @param versification the versification of the passed in key
 501  
      * @param versesKey     the text of the reference we are trying to parse
 502  
      * @return the qualified key representing this
 503  
      */
 504  
     private QualifiedKey getAbsentQualifiedKey(final Versification versification, final String versesKey) {
 505  0
         if (versification.equals(this.nonKjv)) {
 506  
             // we're dealing with a '?', and therefore an ABSENT_IN_LEFT scenarios.
 507  
             // we do not support any other ? markers on the left
 508  0
             return new QualifiedKey();
 509  
         }
 510  
         // we're dealing with a ? on the KJV side, therefore we must be looking at
 511  
         // a section name
 512  0
         return new QualifiedKey(versesKey);
 513  
     }
 514  
 
 515  
     /**
 516  
      * @return the qualified keys associated with the input key.
 517  
      */
 518  
     private List<QualifiedKey> getQualifiedKeys(final Key leftKey) {
 519  0
         return this.toKJVMappings.get(leftKey);
 520  
     }
 521  
 
 522  
     /**
 523  
      * Maps the full qualified key to its proper equivalent in the KJV.
 524  
      *
 525  
      * @param qualifiedKey the qualified key
 526  
      * @return the list of matching qualified keys in the KJV versification.
 527  
      */
 528  
     public List<QualifiedKey> map(QualifiedKey qualifiedKey) {
 529  0
         VerseKey key = qualifiedKey.getKey();
 530  0
         if (key instanceof Verse) {
 531  0
             List<QualifiedKey> kjvKeys = this.getQualifiedKeys(key);
 532  0
             if (kjvKeys == null || kjvKeys.size() == 0) {
 533  
                 //then we found no mapping, so we're essentially going to return the same key back...
 534  
                 //unless it's a verse 0 and then we'll check the global flag.
 535  0
                 kjvKeys = new ArrayList<QualifiedKey>();
 536  0
                 kjvKeys.add(qualifiedKey.reversify(KJV));
 537  0
                 return kjvKeys;
 538  
             }
 539  0
             return kjvKeys;
 540  
         }
 541  
 
 542  0
         return new ArrayList<QualifiedKey>();
 543  
     }
 544  
 
 545  
     /**
 546  
      * Converts a KJV verse to the target versification, from a qualified key, rather than a real key
 547  
      *
 548  
      * @param kjvVerse the verse from the KJV
 549  
      * @return the key in the left-hand versification
 550  
      */
 551  
     public VerseKey unmap(final QualifiedKey kjvVerse) {
 552  
         // TODO(CJB): cope for parts?
 553  0
         Passage left = this.fromKJVMappings.get(kjvVerse);
 554  
 
 555  0
         if (left == null && !kjvVerse.isWhole()) {
 556  
             // Try again, but without the part this time
 557  0
             left = this.fromKJVMappings.get(new QualifiedKey(kjvVerse.getVerse().getWhole()));
 558  
         }
 559  
 
 560  
         //if we have no mapping, then we are in 1 of two scenarios
 561  
         //the verse is either totally absent, or the verse is not part of the mappings, meaning it is a straight map
 562  0
         if (left == null) {
 563  0
             VerseKey vk = kjvVerse.getKey();
 564  0
             if (vk != null && this.absentVerses.contains(vk)) {
 565  0
                 return createEmptyPassage(KJV);
 566  
             }
 567  0
             return kjvVerse.reversify(this.nonKjv).getKey();
 568  
         }
 569  0
         return left;
 570  
     }
 571  
 
 572  
     /**
 573  
      * Outputs the mappings for debug purposes to the log file.
 574  
      */
 575  
     private void trace() {
 576  0
         if (LOGGER.isTraceEnabled()) {
 577  0
             PrintStream ps = null;
 578  
             try {
 579  0
                 ByteArrayOutputStream os = new ByteArrayOutputStream();
 580  0
                 ps = new PrintStream(os);
 581  0
                 dump(ps);
 582  0
                 String output = os.toString("UTF8");
 583  0
                 LOGGER.trace(output);
 584  0
             } catch (UnsupportedEncodingException e) {
 585  
                 // It is impossible!
 586  0
                 LOGGER.error("Encoding UTF8 not supported.", e);
 587  
             } finally {
 588  0
                 IOUtil.close(ps);
 589  0
             }
 590  
         }
 591  0
     }
 592  
 
 593  
     /**
 594  
      * Dump a debug representation of this map to the output stream.
 595  
      * 
 596  
      * @param out
 597  
      */
 598  
     public void dump(PrintStream out) {
 599  0
         String nonKjvName = this.nonKjv.getName();
 600  0
         out.println("##############################");
 601  0
         out.print(String.format("Mapping between KJV and %s", nonKjvName));
 602  0
         out.println("##############################");
 603  0
         out.println("    ******************************");
 604  0
         out.println("    Forward mappings towards KJV");
 605  0
         out.println("    ******************************");
 606  0
         for (Map.Entry<VerseKey, List<QualifiedKey>> entry : this.toKJVMappings.entrySet()) {
 607  0
             List<QualifiedKey> kjvVerses = entry.getValue();
 608  0
             String osisRef = entry.getKey().getOsisRef();
 609  0
             for (QualifiedKey q : kjvVerses) {
 610  0
                 out.println(String.format("\t(%s) %s => (KJV) %s",
 611  
                         nonKjvName,
 612  
                         osisRef,
 613  
                         q.toString()));
 614  
             }
 615  0
         }
 616  
 
 617  0
         out.println("    ******************************");
 618  0
         out.println("    Absent verses in left versification:");
 619  0
         out.println(String.format("\t[%s]", this.absentVerses.getOsisRef()));
 620  0
         out.println("    ******************************");
 621  0
         out.println("    Backwards mappings from KJV");
 622  0
         out.println("    ******************************");
 623  0
         for (Map.Entry<QualifiedKey, Passage> entry : this.fromKJVMappings.entrySet()) {
 624  0
             out.println(String.format("\t(KJV): %s => (%s) %s",
 625  
                     entry.getKey().toString(),
 626  
                     nonKjvName,
 627  
                     entry.getValue().getOsisRef()));
 628  
         }
 629  0
     }
 630  
 
 631  
     /**
 632  
      * Returns whether we initialised with errors
 633  
      */
 634  
     boolean hasErrors() {
 635  0
         return hasErrors;
 636  
     }
 637  
 
 638  
     /** Simplify creation of an empty passage object of the default type, with the required v11n.
 639  
      * 
 640  
      * @param versification required v11n for new Passage
 641  
      * @return              empty Passage
 642  
      */
 643  
     private Passage createEmptyPassage(Versification versification) {
 644  0
         return new RangedPassage(versification);
 645  
     }
 646  
 
 647  
     /* the 'from' or 'left' versification */
 648  
     private Versification nonKjv;
 649  
 
 650  
     /* the absent verses, i.e. those present in the KJV, but not in the left versification */
 651  
     private Passage absentVerses;
 652  
     private Map<VerseKey, List<QualifiedKey>> toKJVMappings;
 653  
     private Map<QualifiedKey, Passage> fromKJVMappings;
 654  
     private boolean hasErrors;
 655  
 
 656  0
     private OsisParser osisParser = new OsisParser();
 657  
 
 658  0
     private static final Versification KJV = Versifications.instance().getVersification(Versifications.DEFAULT_V11N);
 659  0
     private static final Logger LOGGER = LoggerFactory.getLogger(VersificationToKJVMapper.class);
 660  
 }