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