| 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 | } |