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, 2005 - 2016
18   *
19   */
20  package org.crosswire.jsword.book.basic;
21  
22  import java.util.HashSet;
23  import java.util.Iterator;
24  import java.util.LinkedHashSet;
25  import java.util.List;
26  import java.util.Set;
27  
28  import org.crosswire.common.util.StringUtil;
29  import org.crosswire.jsword.book.BookData;
30  import org.crosswire.jsword.book.BookException;
31  import org.crosswire.jsword.book.BookMetaData;
32  import org.crosswire.jsword.book.OSISUtil;
33  import org.crosswire.jsword.book.filter.SourceFilter;
34  import org.crosswire.jsword.book.sword.Backend;
35  import org.crosswire.jsword.book.sword.processing.RawTextToXmlProcessor;
36  import org.crosswire.jsword.passage.Key;
37  import org.crosswire.jsword.passage.KeyUtil;
38  import org.crosswire.jsword.passage.NoSuchKeyException;
39  import org.crosswire.jsword.passage.Passage;
40  import org.crosswire.jsword.passage.PassageKeyFactory;
41  import org.crosswire.jsword.passage.RestrictionType;
42  import org.crosswire.jsword.passage.Verse;
43  import org.crosswire.jsword.passage.VerseKey;
44  import org.crosswire.jsword.passage.VerseRange;
45  import org.crosswire.jsword.versification.BibleBook;
46  import org.crosswire.jsword.versification.Versification;
47  import org.crosswire.jsword.versification.VersificationsMapper;
48  import org.crosswire.jsword.versification.system.Versifications;
49  import org.jdom2.Content;
50  import org.jdom2.Element;
51  import org.slf4j.Logger;
52  import org.slf4j.LoggerFactory;
53  
54  /**
55   * An abstract implementation of Book that lets implementors just concentrate on
56   * reading book data.
57   *
58   * @author Joe Walker
59   * @see gnu.lgpl.License The GNU Lesser General Public License for details.<br>
60   * The copyright to this program is held by its authors.
61   */
62  public abstract class AbstractPassageBook extends AbstractBook {
63  
64      /**
65       * Construct an AbstractPassageBook given the BookMetaData and the AbstractBackend.
66       *
67       * @param bmd     the metadata that describes the book
68       * @param backend the means by which the resource is accessed
69       */
70      public AbstractPassageBook(BookMetaData bmd, Backend backend) {
71          super(bmd, backend);
72          keyf = PassageKeyFactory.instance();
73          this.versification = bmd.getProperty(BookMetaData.KEY_VERSIFICATION);
74      }
75  
76      /* (non-Javadoc)
77       * @see org.crosswire.jsword.book.Book#getOsisIterator(org.crosswire.jsword.passage.Key, boolean, boolean)
78       */
79      public Iterator<Content> getOsisIterator(final Key key, final boolean allowEmpty, final boolean allowGenTitles) throws BookException {
80          // Note: allowEmpty indicates parallel view
81          // TODO(DMS): make the iterator be demand driven
82          final SourceFilter filter = getFilter();
83  
84          // For all the ranges in this Passage
85          //TODO(CJB): I'd prefer to do the key mapping in KeyUtil, and pass in our current versification.
86          //we could remove the method that doesn't support the versification parameter.
87          //but that has far reaching consequences.
88          Passage ref = VersificationsMapper.instance().map(KeyUtil.getPassage(key), this.getVersification());
89  
90          // Generated titles are shown when
91          // there are 2 or more ranges or
92          // empty are not allowed and generated titles are allowed
93          final boolean showTitles = ref.hasRanges(RestrictionType.CHAPTER) || (!allowEmpty && allowGenTitles);
94  
95          RawTextToXmlProcessor processor = new RawTextToXmlProcessor() {
96              // track previous text to exclude duplicates caused by merged verses
97              private String previousVerseText = "";
98  
99              public void preRange(VerseRange range, List<Content> partialDom) {
100                 if (showTitles) {
101                     Element title = OSISUtil.factory().createGeneratedTitle();
102                     title.addContent(range.getName());
103                     partialDom.add(title);
104                 }
105             }
106 
107             public void postVerse(Key verse, List<Content> partialDom, String rawText) {
108                 // If the verse is empty or repeated then we shouldn't add the verse
109                 if ((allowEmpty || rawText.length() > 0) && !previousVerseText.equals(rawText)) {
110                     List<Content> osisContent = filter.toOSIS(AbstractPassageBook.this, verse, rawText);
111                     addOSIS(verse, partialDom, osisContent);
112                 }
113                 previousVerseText = rawText;
114             }
115 
116             public void init(List<Content> partialDom) {
117                 // no-op
118             }
119         };
120 
121         return getOsis(ref, processor).iterator();
122     }
123 
124     /**
125      * Add the OSIS elements to the div element. Note, this assumes that the
126      * data is fully marked up.
127      *
128      * @param key         The key being added
129      * @param div         The div element to which the key's OSIS representation is
130      *                    being added
131      * @param osisContent The OSIS representation of the key being added.
132      */
133     public void addOSIS(Key key, Element div, List<Content> osisContent) {
134         assert key != null;
135         div.addContent(osisContent);
136     }
137 
138     /**
139      * Add the OSIS elements to the content list. Note, this assumes that the
140      * data is fully marked up.
141      *
142      * @param key         The key being added
143      * @param content     The list to which the key's OSIS representation is being added
144      * @param osisContent The OSIS representation of the key being added.
145      */
146     public void addOSIS(Key key, List<Content> content, List<Content> osisContent) {
147         assert key != null;
148         content.addAll(osisContent);
149     }
150 
151     /**
152      * What filter should be used to filter data in the format produced by this
153      * Book?. In some ways this method is more suited to BookMetaData however we
154      * do not have a specialization of BookMetaData to fit AbstractPassageBook
155      * and it doesn't like any higher in the hierarchy at the moment so I will
156      * leave this here.
157      */
158     protected abstract SourceFilter getFilter();
159 
160     /**
161      * For when we want to add writing functionality. This does not work.
162      *
163      * @param key
164      * @param bdata
165      * @throws BookException
166      */
167     public void setDocument(Key key, BookData bdata) throws BookException {
168         // For all of the sections
169         for (Content nextElem : OSISUtil.getFragment(bdata.getOsisFragment())) {
170             if (nextElem instanceof Element) {
171                 Element div = (Element) nextElem;
172 
173                 // For all of the Verses in the section
174                 for (Content data : div.getContent()) {
175                     if (data instanceof Element) {
176                         Element overse = (Element) data;
177                         String text = OSISUtil.getPlainText(overse);
178 
179                         setRawText(key, text);
180                     } else {
181                         log.error("Ignoring non OSIS/Verse content of DIV.");
182                     }
183                 }
184             } else {
185                 log.error("Ignoring non OSIS/Verse content of DIV.");
186             }
187         }
188     }
189 
190     /* (non-Javadoc)
191      * @see org.crosswire.jsword.book.Book#isWritable()
192      */
193     public boolean isWritable() {
194         return false;
195     }
196 
197     /* (non-Javadoc)
198      * @see org.crosswire.jsword.passage.KeyFactory#createEmptyKeyList()
199      */
200     public final Key createEmptyKeyList() {
201         return keyf.createEmptyKeyList(Versifications.instance().getVersification(versification));
202     }
203 
204 
205     /* (non-Javadoc)
206      * @see org.crosswire.jsword.passage.KeyFactory#getValidKey(java.lang.String)
207      */
208     public Key getValidKey(String name) {
209         try {
210             return getKey(name);
211         } catch (NoSuchKeyException e) {
212             return createEmptyKeyList();
213         }
214     }
215 
216     /* (non-Javadoc)
217      * @see org.crosswire.jsword.passage.KeyFactory#getKey(java.lang.String)
218      */
219     public final Key getKey(String text) throws NoSuchKeyException {
220         return PassageKeyFactory.instance().getKey(Versifications.instance().getVersification(versification), text);
221     }
222 
223     public Versification getVersification() {
224         if (this.versificationSystem == null) {
225             this.versificationSystem = Versifications.instance().getVersification(getBookMetaData().getProperty(BookMetaData.KEY_VERSIFICATION));
226         }
227         return versificationSystem;
228     }
229 
230     /**
231      * This implementation lazily inits, saves to the JSword conf file and also caches the book list for future use.
232      *
233      * @return the list of Bible books contained in the module
234      */
235     public Set<BibleBook> getBibleBooks() {
236         if (bibleBooks == null) {
237             synchronized (this) {
238                 if (bibleBooks == null) {
239                     bibleBooks = getBibleBooksInternal();
240                 }
241             }
242         }
243 
244         return bibleBooks;
245     }
246 
247     /**
248      * Obtains the set of bible books from the internal configuration file, creating it if required.
249      * @return the bible books relevant to this module.
250      */
251     private Set<BibleBook> getBibleBooksInternal() {
252         String list = this.getBookMetaData().getProperty(BookMetaData.KEY_BOOKLIST);
253         Set<BibleBook> books;
254         if (list == null) {
255             //calculate and store
256             books = calculateBibleBookList();
257             String listOfBooks = toString(books);
258             this.putProperty(BookMetaData.KEY_BOOKLIST, listOfBooks);
259         } else {
260             //iterate through each item and get the books as a bible books
261             books = fromString(list);
262         }
263 
264         return books;
265     }
266 
267     private Set<BibleBook> fromString(String list) {
268         Set<BibleBook> books = new LinkedHashSet<BibleBook>(list.length() / 2);
269         final String[] bookOsis = StringUtil.split(list, ' ');
270         for (String s : bookOsis) {
271             books.add(BibleBook.fromExactOSIS(s));
272         }
273         return books;
274     }
275 
276     private String toString(Set<BibleBook> books) {
277         StringBuilder sb = new StringBuilder(books.size() * 8);
278         for (Iterator<BibleBook> iterator = books.iterator(); iterator.hasNext(); ) {
279             BibleBook b = iterator.next();
280             sb.append(b.getOSIS());
281             if (iterator.hasNext()) {
282                 sb.append(' ');
283             }
284 
285         }
286         return sb.toString();
287     }
288 
289     /**
290      * Iterate all books checking if document contains a verse from the book
291      */
292     private Set<BibleBook> calculateBibleBookList() {
293         final BookMetaData bookMetaData = this.getBookMetaData();
294         final VerseKey scope = (VerseKey) getScope();
295         if (scope == null) {
296             return new HashSet<BibleBook>();
297         }
298 
299         final Set<BibleBook> bookList = new LinkedHashSet<BibleBook>();
300 
301         // iterate over all book possible in this document
302         final Versification v11n = Versifications.instance().getVersification(bookMetaData.getProperty(BookMetaData.KEY_VERSIFICATION));
303         final Iterator<BibleBook> v11nBookIterator = v11n.getBookIterator();
304 
305         while (v11nBookIterator.hasNext()) {
306             BibleBook bibleBook = v11nBookIterator.next();
307             // test some random verses - normally ch1 v 1 is sufficient - but we don't want to miss any
308             if (scope.contains(new Verse(v11n, bibleBook, 1, 1))
309                 || scope.contains(new Verse(v11n, bibleBook, 1, 2)))
310             {
311                 bookList.add(bibleBook);
312             }
313         }
314 
315         return bookList;
316     }
317 
318     /**
319      * The name of the versification or null
320      */
321     private String versification;
322 
323     /**
324      * Versification system, created lazily, so use getter
325      */
326     private Versification versificationSystem;
327 
328     /**
329      * Our key manager
330      */
331     private PassageKeyFactory keyf;
332 
333     /**
334      * lazy of cache of bible books contained in the Book
335      */
336     private volatile Set<BibleBook> bibleBooks;
337 
338     /**
339      * The log stream
340      */
341     private static final Logger log = LoggerFactory.getLogger(AbstractPassageBook.class);
342 
343 }
344