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     public VersificationToKJVMapper(Versification nonKjv, final FileVersificationMapping mapping) {
133         absentVerses = createEmptyPassage(KJV);
134         toKJVMappings = new HashMap<VerseKey, List<QualifiedKey>>();
135         fromKJVMappings = new HashMap<QualifiedKey, Passage>();
136         this.nonKjv = nonKjv;
137         processMappings(mapping);
138         trace();
139     }
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         final List<KeyValuePair> entries = mappings.getMappings();
148         for (KeyValuePair entry : entries) {
149             try {
150                 processEntry(entry);
151             } catch (NoSuchKeyException ex) {
152                 // TODO(CJB): should we throw a config exception?
153                 LOGGER.error("Unable to process entry [{}] with value [{}]", entry.getKey(), entry.getValue(), ex);
154                 hasErrors = true;
155             }
156         }
157     }
158 
159     private void processEntry(final KeyValuePair entry) throws NoSuchKeyException {
160         String leftHand = entry.getKey();
161         String kjvHand = entry.getValue();
162 
163         if (leftHand == null || leftHand.length() == 0) {
164             LOGGER.error("Left hand must have content");
165             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         if ("!zerosUnmapped".equals(leftHand)) {
171             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         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         QualifiedKey kjv = getRange(KJV, kjvHand, left.getKey());
181         addMappings(left, kjv);
182     }
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         if (leftHand.getAbsentType() == QualifiedKey.Qualifier.ABSENT_IN_LEFT) {
193             this.absentVerses.addAll(kjvVerses.getKey());
194         } else if (leftHand.getKey().getCardinality() == 1) {
195             add1ToManyMappings(leftHand.getVerse(), kjvVerses);
196         } else {
197             addManyToMany(leftHand, kjvVerses);
198         }
199     }
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         VerseKey leftKeys = leftHand.getKey();
210         VerseKey kjvKeys = kjvVerses.getKey();
211         Iterator<Key> leftIter = leftKeys.iterator();
212 
213         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             int diff = Math.abs(leftKeys.getCardinality() - kjvKeys.getCardinality());
216 
217             if (diff > 1) {
218                 reportCardinalityError(leftKeys, kjvKeys);
219             }
220             boolean skipVerse0 = diff == 1;
221 
222             Iterator<Key> kjvIter = kjvKeys.iterator();
223             while (leftIter.hasNext()) {
224                 Verse leftVerse = (Verse) leftIter.next();
225 
226                 // hasNext() and next() have to be paired
227                 if (!kjvIter.hasNext()) {
228                     reportCardinalityError(leftKeys, kjvKeys);
229                 }
230 
231                 Verse rightVerse = (Verse) kjvIter.next();
232                 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                 if (skipVerse0 && leftVerse.getVerse() == 0) {
242 
243                     addForwardMappingFromSingleKeyToRange(leftVerse, kjvKey);
244                     addKJVToMapping(kjvKey, leftVerse);
245 
246                     if (!leftIter.hasNext()) {
247                         reportCardinalityError(leftKeys, kjvKeys);
248                     }
249 
250                     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                 if (skipVerse0 && rightVerse.getVerse() == 0) {
259 
260                     addForwardMappingFromSingleKeyToRange(leftVerse, kjvKey);
261                     addKJVToMapping(kjvKey, leftVerse);
262 
263                     if (!kjvIter.hasNext()) {
264                         reportCardinalityError(leftKeys, kjvKeys);
265                     }
266 
267                     rightVerse = (Verse) kjvIter.next();
268                     kjvKey = new QualifiedKey(rightVerse);
269                 }
270 
271                 // Now do the normal case mapping
272                 addForwardMappingFromSingleKeyToRange(leftVerse, kjvKey);
273                 addKJVToMapping(kjvKey, leftVerse);
274             }
275 
276             // Check to see if, having exhausted left that there is more
277             // on the right
278             if (kjvIter.hasNext()) {
279                 reportCardinalityError(leftKeys, kjvKeys);
280             }
281         } else {
282             while (leftIter.hasNext()) {
283                 final Verse leftKey = (Verse) leftIter.next();
284                 addForwardMappingFromSingleKeyToRange(leftKey, kjvVerses);
285                 addKJVToMapping(kjvVerses, leftKey);
286             }
287         }
288 
289     }
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         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         if (leftKey != null) {
314             getNonEmptyKey(this.fromKJVMappings, kjvVerses).addAll(leftKey);
315 
316             // If we have a part, then we need to add the whole verse as well...
317             if (!kjvVerses.isWhole()) {
318                 getNonEmptyKey(this.fromKJVMappings, QualifiedKey.create(kjvVerses.getKey().getWhole())).addAll(leftKey);
319             }
320         }
321     }
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         addForwardMappingFromSingleKeyToRange(leftHand, kjvHand);
332         addReverse1ToManyMappings(leftHand, kjvHand);
333     }
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         if (kjvHand.getAbsentType() == QualifiedKey.Qualifier.ABSENT_IN_KJV || kjvHand.getKey().getCardinality() == 1) {
344             // TODO(CJB): deal with parts
345             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             Iterator<Key> kjvKeys = kjvHand.getKey().iterator();
351             while (kjvKeys.hasNext()) {
352                 addKJVToMapping(new QualifiedKey(KeyUtil.getVerse(kjvKeys.next())), leftHand);
353             }
354         }
355     }
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         if (leftHand == null) {
365             return;
366         }
367 
368         getNonEmptyMappings(this.toKJVMappings, leftHand).add(kjvHand);
369     }
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         Passage matchingVerses = mappings.get(key);
380         if (matchingVerses == null) {
381             matchingVerses = createEmptyPassage(this.nonKjv);
382             mappings.put(key, matchingVerses);
383         }
384         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         List<S> matchingVerses = mappings.get(key);
398         if (matchingVerses == null) {
399             matchingVerses = new ArrayList<S>();
400             mappings.put(key, matchingVerses);
401         }
402         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         if (versesKey == null || versesKey.length() == 0) {
414             throw new NoSuchKeyException(JSMsg.gettext("Cannot understand [{0}] as a chapter or verse.", versesKey));
415         }
416 
417         char firstChar = versesKey.charAt(0);
418         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                 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                 return getOffsetQualifiedKey(versification, versesKey, offsetBasis);
429             default:
430                 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         if (offsetBasis == null || offsetBasis.getCardinality() == 0) {
443             // TODO(CJB): internationalize
444             throw new NoSuchKeyException(JSMsg.gettext("Unable to offset the given key [{0}]", offsetBasis));
445         }
446         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          VerseRange vr = null;
452          if (offsetBasis instanceof VerseRange) {
453              vr = (VerseRange) offsetBasis;
454          } else if (offsetBasis instanceof Passage) {
455              Iterator iter = ((Passage) offsetBasis).rangeIterator(RestrictionType.NONE);
456              if (iter.hasNext()) {
457                  vr = (VerseRange) iter.next();
458              }
459          }
460          if (vr == null) {
461              // TODO(CJB): internationalize
462              throw new NoSuchKeyException(JSMsg.gettext("Unable to offset the given key [{0}]", offsetBasis));
463          }
464 
465          Verse vrStart = vr.getStart();
466          Verse start = vrStart.reversify(versification);
467          // While you can add a negative number, these are optimized for their operation
468          if (offset < 0) {
469              start = versification.subtract(start, -offset);
470          } else if (offset > 0) {
471              start = versification.add(start, offset);
472          }
473          Verse end = start;
474          if (vr.getCardinality() > 1) {
475              end = versification.add(start, vr.getCardinality() - 1);
476          }
477 
478         if (start == null || end == null) {
479             hasErrors = true;
480             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          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         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         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             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         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         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         VerseKey key = qualifiedKey.getKey();
530         if (key instanceof Verse) {
531             List<QualifiedKey> kjvKeys = this.getQualifiedKeys(key);
532             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                 kjvKeys = new ArrayList<QualifiedKey>();
536                 kjvKeys.add(qualifiedKey.reversify(KJV));
537                 return kjvKeys;
538             }
539             return kjvKeys;
540         }
541 
542         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         Passage left = this.fromKJVMappings.get(kjvVerse);
554 
555         if (left == null && !kjvVerse.isWhole()) {
556             // Try again, but without the part this time
557             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         if (left == null) {
563             VerseKey vk = kjvVerse.getKey();
564             if (vk != null && this.absentVerses.contains(vk)) {
565                 return createEmptyPassage(KJV);
566             }
567             return kjvVerse.reversify(this.nonKjv).getKey();
568         }
569         return left;
570     }
571 
572     /**
573      * Outputs the mappings for debug purposes to the log file.
574      */
575     private void trace() {
576         if (LOGGER.isTraceEnabled()) {
577             PrintStream ps = null;
578             try {
579                 ByteArrayOutputStream os = new ByteArrayOutputStream();
580                 ps = new PrintStream(os);
581                 dump(ps);
582                 String output = os.toString("UTF8");
583                 LOGGER.trace(output);
584             } catch (UnsupportedEncodingException e) {
585                 // It is impossible!
586                 LOGGER.error("Encoding UTF8 not supported.", e);
587             } finally {
588                 IOUtil.close(ps);
589             }
590         }
591     }
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         String nonKjvName = this.nonKjv.getName();
600         out.println("##############################");
601         out.print(String.format("Mapping between KJV and %s", nonKjvName));
602         out.println("##############################");
603         out.println("    ******************************");
604         out.println("    Forward mappings towards KJV");
605         out.println("    ******************************");
606         for (Map.Entry<VerseKey, List<QualifiedKey>> entry : this.toKJVMappings.entrySet()) {
607             List<QualifiedKey> kjvVerses = entry.getValue();
608             String osisRef = entry.getKey().getOsisRef();
609             for (QualifiedKey q : kjvVerses) {
610                 out.println(String.format("\t(%s) %s => (KJV) %s",
611                         nonKjvName,
612                         osisRef,
613                         q.toString()));
614             }
615         }
616 
617         out.println("    ******************************");
618         out.println("    Absent verses in left versification:");
619         out.println(String.format("\t[%s]", this.absentVerses.getOsisRef()));
620         out.println("    ******************************");
621         out.println("    Backwards mappings from KJV");
622         out.println("    ******************************");
623         for (Map.Entry<QualifiedKey, Passage> entry : this.fromKJVMappings.entrySet()) {
624             out.println(String.format("\t(KJV): %s => (%s) %s",
625                     entry.getKey().toString(),
626                     nonKjvName,
627                     entry.getValue().getOsisRef()));
628         }
629     }
630 
631     /**
632      * Returns whether we initialised with errors
633      */
634     boolean hasErrors() {
635         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         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     private OsisParser osisParser = new OsisParser();
657 
658     private static final Versification KJV = Versifications.instance().getVersification(Versifications.DEFAULT_V11N);
659     private static final Logger LOGGER = LoggerFactory.getLogger(VersificationToKJVMapper.class);
660 }
661