VersificationsMapper.java |
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 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 MAPPERS.put(KJV, null); 71 } 72 73 /** 74 * @return a singleton instance of the mapper - 75 */ 76 public static VersificationsMapper instance() { 77 if (instance == null) { 78 synchronized (VersificationsMapper.class) { 79 if (instance == null) { 80 instance = new VersificationsMapper(); 81 } 82 } 83 } 84 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 if (key.getVersification().equals(target)) { 99 return key; 100 } 101 102 Passage newPassage = new RangedPassage(target); 103 Iterator<Key> verses = key.iterator(); 104 while (verses.hasNext()) { 105 Verse verse = (Verse) verses.next(); 106 newPassage.addAll(this.mapVerse(verse, target)); 107 } 108 109 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 if (v.getVersification().equals(targetVersification)) { 119 return v; 120 } 121 122 ensure(v.getVersification()); 123 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 VersificationToKJVMapper mapper = MAPPERS.get(v.getVersification()); 128 129 // mapped verses could be more than 1 verse in KJV 130 List<QualifiedKey> kjvVerses; 131 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 kjvVerses = new ArrayList<QualifiedKey>(); 137 final Verse reversifiedVerse = v.reversify(KJV); 138 //check that the key actually exists 139 if (reversifiedVerse != null) { 140 kjvVerses.add(new QualifiedKey(reversifiedVerse)); 141 } 142 } else { 143 //we need qualified keys back, so as to preserve parts 144 kjvVerses = mapper.map(new QualifiedKey(v)); 145 } 146 147 if (KJV.equals(targetVersification)) { 148 // we're done, so simply return the key we have so far. 149 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 VersificationToKJVMapper targetMapper = MAPPERS.get(targetVersification); 155 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 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 VerseKey finalKeys = new RangedPassage(targetVersification); 166 for (QualifiedKey qualifiedKey : kjvVerses) { 167 final VerseKey verseKey = targetMapper.unmap(qualifiedKey); 168 if (verseKey != null) { 169 //verse key exists in the target versification 170 finalKeys.addAll(verseKey); 171 } 172 } 173 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 final VerseKey finalKeys = new RangedPassage(targetVersification); 187 for (QualifiedKey qualifiedKey : kjvVerses) { 188 if (qualifiedKey.getKey() != null) { 189 final VerseKey key = qualifiedKey.reversify(targetVersification).getKey(); 190 if (key != null) { 191 //verse key exists in target versification 192 finalKeys.addAll(key); 193 } 194 } 195 } 196 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 final VerseKey finalKey = new RangedPassage(versification); 205 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 if (k.getKey() != null) { 210 finalKey.addAll(k.getKey()); 211 } 212 } 213 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 ensure(versification); 224 } 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 if (MAPPERS.containsKey(versification)) { 233 return; 234 } 235 236 try { 237 MAPPERS.put(versification, new VersificationToKJVMapper(versification, new FileVersificationMapping(versification))); 238 } catch (IOException e) { 239 // we've attempted to load it once, and that's all we'll do. 240 LOGGER.error("Failed to load versification mappings for versification [{}]", versification, e); 241 MAPPERS.put(versification, null); 242 } catch (ConfigException e) { 243 // we've attempted to load it once, and that's all we'll do. 244 LOGGER.error("Failed to load versification mappings for versification [{}]", versification, e); 245 MAPPERS.put(versification, null); 246 } catch (MissingResourceException e) { 247 // we've attempted to load it once, and that's all we'll do. 248 LOGGER.error("Failed to load versification mappings for versification [{}]", versification, e); 249 MAPPERS.put(versification, null); 250 } 251 } 252 253 private static volatile VersificationsMapper instance; 254 private static final Versification KJV = Versifications.instance().getVersification(Versifications.DEFAULT_V11N); 255 private static final Map<Versification, VersificationToKJVMapper> MAPPERS = new HashMap<Versification, VersificationToKJVMapper>(); 256 private static final Logger LOGGER = LoggerFactory.getLogger(VersificationsMapper.class); 257 } 258