Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
VersificationToKJVMapper |
|
| 4.217391304347826;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 & 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=>KJV and KJV=>V2 gives you Gen.1.1=>Gen.1.1=>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=>KJV and KJV=>V2 gives you Gen.1.1=>Gen.1.1!a=>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 | } |