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 =&gt; 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