1
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
126 public class VersificationToKJVMapper {
127
128
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
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 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 if ("!zerosUnmapped".equals(leftHand)) {
171 return;
172 }
173
174 QualifiedKey left = getRange(this.nonKjv, leftHand, null);
177
178 QualifiedKey kjv = getRange(KJV, kjvHand, left.getKey());
181 addMappings(left, kjv);
182 }
183
184
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
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 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 if (!kjvIter.hasNext()) {
228 reportCardinalityError(leftKeys, kjvKeys);
229 }
230
231 Verse rightVerse = (Verse) kjvIter.next();
232 QualifiedKey kjvKey = new QualifiedKey(rightVerse);
233
234 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 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 addForwardMappingFromSingleKeyToRange(leftVerse, kjvKey);
273 addKJVToMapping(kjvKey, leftVerse);
274 }
275
276 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
297 private void reportCardinalityError(final VerseKey leftKeys, final VerseKey kjvKeys) {
298 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
311 private void addKJVToMapping(final QualifiedKey kjvVerses, final Verse leftKey) {
312 if (leftKey != null) {
314 getNonEmptyKey(this.fromKJVMappings, kjvVerses).addAll(leftKey);
315
316 if (!kjvVerses.isWhole()) {
318 getNonEmptyKey(this.fromKJVMappings, QualifiedKey.create(kjvVerses.getKey().getWhole())).addAll(leftKey);
319 }
320 }
321 }
322
323
330 private void add1ToManyMappings(final Verse leftHand, final QualifiedKey kjvHand) throws NoSuchVerseException {
331 addForwardMappingFromSingleKeyToRange(leftHand, kjvHand);
332 addReverse1ToManyMappings(leftHand, kjvHand);
333 }
334
335
341 private void addReverse1ToManyMappings(final Verse leftHand, final QualifiedKey kjvHand) {
342 if (kjvHand.getAbsentType() == QualifiedKey.Qualifier.ABSENT_IN_KJV || kjvHand.getKey().getCardinality() == 1) {
344 addKJVToMapping(kjvHand, leftHand);
346 } else {
347 Iterator<Key> kjvKeys = kjvHand.getKey().iterator();
351 while (kjvKeys.hasNext()) {
352 addKJVToMapping(new QualifiedKey(KeyUtil.getVerse(kjvKeys.next())), leftHand);
353 }
354 }
355 }
356
357
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
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
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
411 private QualifiedKey getRange(final Versification versification, String versesKey, VerseKey offsetBasis) throws NoSuchKeyException {
412 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 return getAbsentQualifiedKey(versification, versesKey);
424 case '+':
425 case '-':
426 return getOffsetQualifiedKey(versification, versesKey, offsetBasis);
429 default:
430 return getExistingQualifiedKey(versification, versesKey);
431 }
432 }
433
434
441 private QualifiedKey getOffsetQualifiedKey(final Versification versification, final String versesKey, VerseKey offsetBasis) throws NoSuchKeyException {
442 if (offsetBasis == null || offsetBasis.getCardinality() == 0) {
443 throw new NoSuchKeyException(JSMsg.gettext("Unable to offset the given key [{0}]", offsetBasis));
445 }
446 int offset = Integer.parseInt(versesKey.substring(1));
447
448
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 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 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
493 private QualifiedKey getExistingQualifiedKey(final Versification versification, final String versesKey) {
494 return new QualifiedKey(osisParser.parseOsisRef(versification, versesKey));
495 }
496
497
504 private QualifiedKey getAbsentQualifiedKey(final Versification versification, final String versesKey) {
505 if (versification.equals(this.nonKjv)) {
506 return new QualifiedKey();
509 }
510 return new QualifiedKey(versesKey);
513 }
514
515
518 private List<QualifiedKey> getQualifiedKeys(final Key leftKey) {
519 return this.toKJVMappings.get(leftKey);
520 }
521
522
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 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
551 public VerseKey unmap(final QualifiedKey kjvVerse) {
552 Passage left = this.fromKJVMappings.get(kjvVerse);
554
555 if (left == null && !kjvVerse.isWhole()) {
556 left = this.fromKJVMappings.get(new QualifiedKey(kjvVerse.getVerse().getWhole()));
558 }
559
560 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
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 LOGGER.error("Encoding UTF8 not supported.", e);
587 } finally {
588 IOUtil.close(ps);
589 }
590 }
591 }
592
593
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
634 boolean hasErrors() {
635 return hasErrors;
636 }
637
638
643 private Passage createEmptyPassage(Versification versification) {
644 return new RangedPassage(versification);
645 }
646
647
648 private Versification nonKjv;
649
650
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