| Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
| VersificationsMapper |
|
| 4.125;4.125 |
| 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.IOException; | |
| 23 | import java.util.ArrayList; | |
| 24 | import java.util.HashMap; | |
| 25 | import java.util.Iterator; | |
| 26 | import java.util.List; | |
| 27 | import java.util.Map; | |
| 28 | import java.util.MissingResourceException; | |
| 29 | ||
| 30 | import org.crosswire.common.config.ConfigException; | |
| 31 | import org.crosswire.jsword.passage.Key; | |
| 32 | import org.crosswire.jsword.passage.Passage; | |
| 33 | import org.crosswire.jsword.passage.RangedPassage; | |
| 34 | import org.crosswire.jsword.passage.Verse; | |
| 35 | import org.crosswire.jsword.passage.VerseKey; | |
| 36 | import org.crosswire.jsword.versification.system.Versifications; | |
| 37 | import org.slf4j.Logger; | |
| 38 | import org.slf4j.LoggerFactory; | |
| 39 | ||
| 40 | /** | |
| 41 | * VersificationMapper maps a Verse or a Passage in one Versification (v11n) | |
| 42 | * to another, using the KJV v11n as an intermediary. | |
| 43 | * <p> | |
| 44 | * Practically speaking, a Verse in one v11n may map: | |
| 45 | * <ul> | |
| 46 | * <li>to the same verse in another v11n. This is the typical case.</li> | |
| 47 | * <li>to another verse in the same chapter. This is common and typically by one, shifting following verses.</li> | |
| 48 | * <li>to another verse in a different chapter. This is fairly common.</li> | |
| 49 | * <li>to two other verses. This is common in the Psalms and a few places elsewhere.</li> | |
| 50 | * </ul> | |
| 51 | * <p> | |
| 52 | * The internal details of the mapping can be found in VersificationToKJVMapper. | |
| 53 | * </p> | |
| 54 | * <p> | |
| 55 | * This transitive relationship is not perfect. It assumes that verses | |
| 56 | * outside of the KJV versification map 1:1 between the source and | |
| 57 | * target Versifications. That it uses the KJV as an intermediary is an | |
| 58 | * implementation detail that may change. Do not rely on it. | |
| 59 | * </p> | |
| 60 | * @see gnu.lgpl.License The GNU Lesser General Public License for details. | |
| 61 | * @author Chris Burrell | |
| 62 | */ | |
| 63 | public final class VersificationsMapper { | |
| 64 | /** | |
| 65 | * Prevent instantiation | |
| 66 | */ | |
| 67 | 0 | private VersificationsMapper() { |
| 68 | // we have no mapper for the KJV, since everything maps map to the KJV, so we'll simply add an entry | |
| 69 | // in there to avoid ever trying to load it | |
| 70 | 0 | MAPPERS.put(KJV, null); |
| 71 | 0 | } |
| 72 | ||
| 73 | /** | |
| 74 | * @return a singleton instance of the mapper - | |
| 75 | */ | |
| 76 | public static VersificationsMapper instance() { | |
| 77 | 0 | if (instance == null) { |
| 78 | 0 | synchronized (VersificationsMapper.class) { |
| 79 | 0 | if (instance == null) { |
| 80 | 0 | instance = new VersificationsMapper(); |
| 81 | } | |
| 82 | 0 | } |
| 83 | } | |
| 84 | 0 | return instance; |
| 85 | } | |
| 86 | ||
| 87 | /** | |
| 88 | * Maps a whole passage, and does so verse by verse. We can't do any better, since, we may for | |
| 89 | * example have: | |
| 90 | * Ps.1.1-Ps.1.10 => Ps.1.2-Ps.1.11 so one would think we can simply map each of the start and end verses. | |
| 91 | * However, this would be inaccurate since verse 9 might map to verse 12, 13, etc. | |
| 92 | * | |
| 93 | * @param key the key if the source versification | |
| 94 | * @param target the target versification | |
| 95 | * @return the new key in the new versification | |
| 96 | */ | |
| 97 | public Passage map(final Passage key, final Versification target) { | |
| 98 | 0 | if (key.getVersification().equals(target)) { |
| 99 | 0 | return key; |
| 100 | } | |
| 101 | ||
| 102 | 0 | Passage newPassage = new RangedPassage(target); |
| 103 | 0 | Iterator<Key> verses = key.iterator(); |
| 104 | 0 | while (verses.hasNext()) { |
| 105 | 0 | Verse verse = (Verse) verses.next(); |
| 106 | 0 | newPassage.addAll(this.mapVerse(verse, target)); |
| 107 | 0 | } |
| 108 | ||
| 109 | 0 | return newPassage; |
| 110 | } | |
| 111 | ||
| 112 | /** | |
| 113 | * @param v the verse | |
| 114 | * @param targetVersification the final versification that we want | |
| 115 | * @return the key for the verse | |
| 116 | */ | |
| 117 | public VerseKey mapVerse(Verse v, Versification targetVersification) { | |
| 118 | 0 | if (v.getVersification().equals(targetVersification)) { |
| 119 | 0 | return v; |
| 120 | } | |
| 121 | ||
| 122 | 0 | ensure(v.getVersification()); |
| 123 | 0 | ensure(targetVersification); |
| 124 | ||
| 125 | // caution, mappers can be null if they are missing or failed to load. | |
| 126 | // get the source mapper, to get to the KJV | |
| 127 | 0 | VersificationToKJVMapper mapper = MAPPERS.get(v.getVersification()); |
| 128 | ||
| 129 | // mapped verses could be more than 1 verse in KJV | |
| 130 | List<QualifiedKey> kjvVerses; | |
| 131 | 0 | if (mapper == null) { |
| 132 | // we can't map to the KJV, so we're going to take a wild guess | |
| 133 | // and return the equivalent verse | |
| 134 | // and assume that it maps directly on to the KJV, | |
| 135 | // and thereby continue with the process | |
| 136 | 0 | kjvVerses = new ArrayList<QualifiedKey>(); |
| 137 | 0 | final Verse reversifiedVerse = v.reversify(KJV); |
| 138 | //check that the key actually exists | |
| 139 | 0 | if (reversifiedVerse != null) { |
| 140 | 0 | kjvVerses.add(new QualifiedKey(reversifiedVerse)); |
| 141 | } | |
| 142 | 0 | } else { |
| 143 | //we need qualified keys back, so as to preserve parts | |
| 144 | 0 | kjvVerses = mapper.map(new QualifiedKey(v)); |
| 145 | } | |
| 146 | ||
| 147 | 0 | if (KJV.equals(targetVersification)) { |
| 148 | // we're done, so simply return the key we have so far. | |
| 149 | 0 | return getKeyFromQualifiedKeys(KJV, kjvVerses); |
| 150 | } | |
| 151 | ||
| 152 | // we're continuing, so we need to unmap from the KJV qualified key onto | |
| 153 | // the new versification. | |
| 154 | 0 | VersificationToKJVMapper targetMapper = MAPPERS.get(targetVersification); |
| 155 | 0 | if (targetMapper == null) { |
| 156 | // failed to load, so we'll do our wild-guess again, | |
| 157 | // and assume that the KJV keys map to the target | |
| 158 | 0 | return guessKeyFromKjvVerses(targetVersification, kjvVerses); |
| 159 | } | |
| 160 | ||
| 161 | // we can use the unmap method for that. Since we have a list of | |
| 162 | // qualified keys, we do so for every qualified | |
| 163 | // key in the list - this means that parts would get transported as | |
| 164 | // well. | |
| 165 | 0 | VerseKey finalKeys = new RangedPassage(targetVersification); |
| 166 | 0 | for (QualifiedKey qualifiedKey : kjvVerses) { |
| 167 | 0 | final VerseKey verseKey = targetMapper.unmap(qualifiedKey); |
| 168 | 0 | if (verseKey != null) { |
| 169 | //verse key exists in the target versification | |
| 170 | 0 | finalKeys.addAll(verseKey); |
| 171 | } | |
| 172 | 0 | } |
| 173 | 0 | return finalKeys; |
| 174 | } | |
| 175 | ||
| 176 | /** | |
| 177 | * This is a last attempt at trying to get something, on the basis that | |
| 178 | * something is better than nothing. | |
| 179 | * | |
| 180 | * @param targetVersification the target versification | |
| 181 | * @param kjvVerses the verses in the KJV versification. | |
| 182 | * @return the possible verses in the target versification, no guarantees | |
| 183 | * made | |
| 184 | */ | |
| 185 | private VerseKey guessKeyFromKjvVerses(final Versification targetVersification, final List<QualifiedKey> kjvVerses) { | |
| 186 | 0 | final VerseKey finalKeys = new RangedPassage(targetVersification); |
| 187 | 0 | for (QualifiedKey qualifiedKey : kjvVerses) { |
| 188 | 0 | if (qualifiedKey.getKey() != null) { |
| 189 | 0 | final VerseKey key = qualifiedKey.reversify(targetVersification).getKey(); |
| 190 | 0 | if (key != null) { |
| 191 | //verse key exists in target versification | |
| 192 | 0 | finalKeys.addAll(key); |
| 193 | } | |
| 194 | 0 | } |
| 195 | } | |
| 196 | 0 | return finalKeys; |
| 197 | } | |
| 198 | ||
| 199 | /** | |
| 200 | * @param kjvVerses the list of keys | |
| 201 | * @return the aggregate key | |
| 202 | */ | |
| 203 | private VerseKey getKeyFromQualifiedKeys(Versification versification, final List<QualifiedKey> kjvVerses) { | |
| 204 | 0 | final VerseKey finalKey = new RangedPassage(versification); |
| 205 | 0 | for (QualifiedKey k : kjvVerses) { |
| 206 | // we simply ignore everything else at this stage. The other bits | |
| 207 | // and pieces are used while we're converting | |
| 208 | // from one to the other. | |
| 209 | 0 | if (k.getKey() != null) { |
| 210 | 0 | finalKey.addAll(k.getKey()); |
| 211 | } | |
| 212 | } | |
| 213 | 0 | return finalKey; |
| 214 | } | |
| 215 | ||
| 216 | /** | |
| 217 | * Call this to ensure mapping data is loaded (maybe for newly installed books). | |
| 218 | * Should normally be called from a background thread, not the ui thread. | |
| 219 | ||
| 220 | * @param versification the versification we want to load mapping data for | |
| 221 | */ | |
| 222 | public void ensureMappingDataLoaded(Versification versification) { | |
| 223 | 0 | ensure(versification); |
| 224 | 0 | } |
| 225 | ||
| 226 | /** | |
| 227 | * Reads the mapping from file if it does not exist | |
| 228 | * | |
| 229 | * @param versification the versification we want to load | |
| 230 | */ | |
| 231 | private void ensure(final Versification versification) { | |
| 232 | 0 | if (MAPPERS.containsKey(versification)) { |
| 233 | 0 | return; |
| 234 | } | |
| 235 | ||
| 236 | try { | |
| 237 | 0 | MAPPERS.put(versification, new VersificationToKJVMapper(versification, new FileVersificationMapping(versification))); |
| 238 | 0 | } catch (IOException e) { |
| 239 | // we've attempted to load it once, and that's all we'll do. | |
| 240 | 0 | LOGGER.error("Failed to load versification mappings for versification [{}]", versification, e); |
| 241 | 0 | MAPPERS.put(versification, null); |
| 242 | 0 | } catch (ConfigException e) { |
| 243 | // we've attempted to load it once, and that's all we'll do. | |
| 244 | 0 | LOGGER.error("Failed to load versification mappings for versification [{}]", versification, e); |
| 245 | 0 | MAPPERS.put(versification, null); |
| 246 | 0 | } catch (MissingResourceException e) { |
| 247 | // we've attempted to load it once, and that's all we'll do. | |
| 248 | 0 | LOGGER.error("Failed to load versification mappings for versification [{}]", versification, e); |
| 249 | 0 | MAPPERS.put(versification, null); |
| 250 | 0 | } |
| 251 | 0 | } |
| 252 | ||
| 253 | private static volatile VersificationsMapper instance; | |
| 254 | 0 | private static final Versification KJV = Versifications.instance().getVersification(Versifications.DEFAULT_V11N); |
| 255 | 0 | private static final Map<Versification, VersificationToKJVMapper> MAPPERS = new HashMap<Versification, VersificationToKJVMapper>(); |
| 256 | 0 | private static final Logger LOGGER = LoggerFactory.getLogger(VersificationsMapper.class); |
| 257 | } |