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.sword;
21  
22  import java.util.List;
23  
24  import org.crosswire.jsword.JSOtherMsg;
25  import org.crosswire.jsword.book.BookException;
26  import org.crosswire.jsword.book.BookMetaData;
27  import org.crosswire.jsword.book.OSISUtil;
28  import org.crosswire.jsword.book.basic.AbstractPassageBook;
29  import org.crosswire.jsword.book.filter.SourceFilter;
30  import org.crosswire.jsword.book.sword.processing.RawTextToXmlProcessor;
31  import org.crosswire.jsword.passage.Key;
32  import org.crosswire.jsword.passage.KeyUtil;
33  import org.crosswire.jsword.passage.NoSuchKeyException;
34  import org.crosswire.jsword.passage.PassageKeyFactory;
35  import org.crosswire.jsword.passage.VerseKey;
36  import org.crosswire.jsword.versification.Versification;
37  import org.jdom2.Attribute;
38  import org.jdom2.Content;
39  import org.jdom2.Element;
40  import org.slf4j.Logger;
41  import org.slf4j.LoggerFactory;
42  
43  /**
44   * SwordBook is a base class for all verse based Sword type books.
45   * 
46   * @see gnu.lgpl.License The GNU Lesser General Public License for details.
47   * @author Joe Walker
48   */
49  public class SwordBook extends AbstractPassageBook {
50      /**
51       * Construct an SwordBook given the BookMetaData and the AbstractBackend.
52       * 
53       * @param sbmd the metadata that describes the book
54       * @param backend the means by which the resource is accessed
55       */
56      public SwordBook(SwordBookMetaData sbmd, Backend<?> backend) {
57          super(sbmd, backend);
58  
59          this.filter = sbmd.getFilter();
60  
61          if (backend == null) {
62              throw new IllegalArgumentException("AbstractBackend must not be null.");
63          }
64      }
65  
66      /* (non-Javadoc)
67       * @see org.crosswire.jsword.book.Book#getGlobalKeyList()
68       */
69      public final Key getGlobalKeyList() {
70          if (global == null) {
71              try {
72                  global = getBackend().getGlobalKeyList();
73                  return global;
74              } catch (UnsupportedOperationException ex) {
75                  // fail silently, operation not supported by the backend
76                  log.debug(ex.getMessage());
77              } catch (BookException ex) {
78                  // failing silently, as previous behavior was to attempt to
79                  // return as much as we can using the slower method
80                  log.debug(ex.getMessage());
81              }
82  
83              Versification v11n = super.getVersification();
84  
85              global = super.createEmptyKeyList();
86              Key all = PassageKeyFactory.instance().getGlobalKeyList(v11n);
87  
88              for (Key key : all) {
89                  if (contains(key)) {
90                      global.addAll(key);
91                  }
92              }
93          }
94  
95          return global;
96      }
97  
98      /* (non-Javadoc)
99       * @see org.crosswire.jsword.book.basic.AbstractBook#getScope()
100      */
101     @Override
102     public Key getScope() {
103         SwordBookMetaData sbmd = (SwordBookMetaData) getBookMetaData();
104         //if the book type doesn't have verses, then leave it.
105         if (sbmd.getProperty(BookMetaData.KEY_VERSIFICATION) == null) {
106             //then we're not looking at a versified book
107             return null;
108         }
109 
110         Object keyString = sbmd.getProperty(BookMetaData.KEY_SCOPE);
111 
112         if (keyString != null) {
113             try {
114                 return getKey((String) keyString);
115             } catch (NoSuchKeyException ex) {
116                 //the scope defined is not correct
117                 log.error("Unable to parse scope from book", ex);
118                 return null;
119             }
120         }
121 
122         //need to calculate the scope
123         //now comes the expensive part
124         Key bookKeys = getGlobalKeyList();
125 
126         //this is practically impossible, but cater for it just in case.
127         if (!(bookKeys instanceof VerseKey)) {
128             log.error("Global key list isn't a verse key. A very expensive no-op has just occurred.");
129             return null;
130         }
131 
132         return bookKeys;
133     }
134 
135     /* (non-Javadoc)
136      * @see org.crosswire.jsword.book.Book#contains(org.crosswire.jsword.passage.Key)
137      */
138     public boolean contains(Key key) {
139         return getBackend().contains(key);
140     }
141 
142     /* (non-Javadoc)
143      * @see org.crosswire.jsword.book.Book#getRawText(org.crosswire.jsword.passage.Key)
144      */
145     public String getRawText(Key key) throws BookException {
146         return getBackend().getRawText(key);
147     }
148 
149     @Override
150     protected List<Content> getOsis(Key key, RawTextToXmlProcessor processor) throws BookException {
151         List<Content> result = getBackend().readToOsis(key, processor);
152         assert result != null;
153         return result;
154     }
155 
156     @Override
157     public void addOSIS(Key key, Element div, List<Content> osisContent) {
158         // See if the text is marked up with verses
159         // If it is then just add it.
160         for (Content content : osisContent) {
161             if (content instanceof Element) {
162                 Element ele = (Element) content;
163                 if (ele.getName().equals(OSISUtil.OSIS_ELEMENT_VERSE)) {
164                     super.addOSIS(key, div, osisContent);
165                     return;
166                 }
167             }
168         }
169 
170         // If we get here then the text is not marked up with verse
171         // In this case we add the verse markup, if the verse is not 0.
172         if (KeyUtil.getVerse(key).getVerse() == 0) {
173             super.addOSIS(key, div, osisContent);
174         } else {
175             // In a SWORD module, the verse element is to be put
176             // after the "preverse" material.
177             Element everse = OSISUtil.factory().createVerse();
178             everse.setAttribute(OSISUtil.OSIS_ATTR_OSISID, key.getOsisID());
179             div.addContent(everse);
180             super.addOSIS(key, everse, osisContent);
181         }
182     }
183 
184     @Override
185     public void addOSIS(Key key, List<Content> contentList, List<Content> osisContent) {
186         // If there is no content, then there is nothing to do.
187         if (osisContent.size() == 0) {
188             return;
189         }
190 
191         // Note: Verse 0 is an introduction and not a verse so it never gets verse markup.
192         // However, it should be wrapped by a div as it should be isolated as an intro.
193         if (KeyUtil.getVerse(key).getVerse() == 0) {
194             Element div = OSISUtil.factory().createDiv();
195             div.setAttribute(OSISUtil.OSIS_ATTR_OSISID, key.getOsisID());
196             // Mark it as generated
197             div.setAttribute(OSISUtil.OSIS_ATTR_TYPE, OSISUtil.GENERATED_CONTENT);
198             // Put the OSIS into the div
199             div.addContent(osisContent);
200             // Then put the div at the end of the contentList
201             contentList.add(div);
202             return;
203         }
204 
205         // SWORD modules typically do not have the verse marker. When they don't
206         // then the verse element needs to wrap the verse content.
207         // However, the verse content may have "pre-verse" content.
208         // This is marked up in one of two ways:
209         // 1) old way
210         //      <title subType="x-preverse">...</title>
211         //    There may be more than one title marked as such.
212         // 2) current way
213         //      <div sID="xxx" type="x-milestone" subType="x-preverse"/>
214         //         ...
215         //      <div eID="xxx" type="x-milestone" subType="x-preverse"/>
216         //      verse content
217         //    In this we only need to look for the ending.
218         // The critical observation is that the verse marker is to
219         // follow the last element marked x-preverse.
220         // Also, there are a good number of modules that have a title marked
221         // type="psalm" and not canonical="true" which they should be.
222 
223         // See if the text is marked up with verse elements
224         // If it is then just add it.
225         int start = 0;
226         int found = -1;
227         boolean wrapped = false;
228         Element preverse = null;
229         for (Content content : osisContent) {
230             if (content instanceof Element) {
231                 Element ele = (Element) content;
232                 String name = ele.getName();
233                 if (OSISUtil.OSIS_ELEMENT_VERSE.equals(name)) {
234                     wrapped = true;
235                     continue;
236                 }
237                 Attribute typeAttr = ele.getAttribute(OSISUtil.OSIS_ATTR_TYPE);
238                 Attribute subTypeAttr = ele.getAttribute(OSISUtil.OSIS_ATTR_SUBTYPE);
239                 if (subTypeAttr != null && "x-preverse".equals(subTypeAttr.getValue())) {
240                     if (OSISUtil.OSIS_ELEMENT_DIV.equals(name) || OSISUtil.OSIS_ELEMENT_TITLE.equals(name)) {
241                         preverse = ele;
242                         found = start;
243                     }
244                 } else if (typeAttr != null && "psalm".equals(typeAttr.getValue()) && OSISUtil.OSIS_ELEMENT_TITLE.equals(name)) {
245                     // Psalm titles should be both canonical and preverse
246                     // set the appropriate attributes if not already set.
247                     Attribute canonicalAttr = ele.getAttribute(OSISUtil.OSIS_ATTR_CANONICAL);
248                     if (canonicalAttr == null) {
249                         ele.setAttribute(OSISUtil.OSIS_ATTR_CANONICAL, "true");
250                     }
251                     if (subTypeAttr == null) {
252                         ele.setAttribute(OSISUtil.OSIS_ATTR_SUBTYPE, "x-preverse");
253                         preverse = ele;
254                         found = start;
255                     }
256                 }
257             }
258             start++;
259         }
260 
261         // Check to see whether the text is marked up with verse
262         if (wrapped) {
263             super.addOSIS(key, contentList, osisContent);
264             return;
265         }
266 
267         // If we get here then the text is not marked up with verse
268         // In this case we add the verse markup, if the verse is not 0.
269         Element everse = OSISUtil.factory().createVerse();
270         everse.setAttribute(OSISUtil.OSIS_ATTR_OSISID, key.getOsisID());
271         if (preverse == null) {
272             everse.addContent(osisContent);
273         } else {
274             List<Content> sublist = osisContent.subList(found + 1, osisContent.size());
275             everse.addContent(sublist);
276             // a sub list is actually part of the original list
277             // clearing it removes it from the original list
278             sublist.clear();
279             // Now append shortened list
280             super.addOSIS(key, contentList, osisContent);
281         }
282         // Then put the verse at the end of the contentList
283         contentList.add(everse);
284     }
285 
286     @Override
287     public boolean isWritable() {
288         return getBackend().isWritable();
289     }
290 
291     /* (non-Javadoc)
292      * @see org.crosswire.jsword.book.Book#setRawText(org.crosswire.jsword.passage.Key, java.lang.String)
293      */
294     public void setRawText(Key key, String rawData) throws BookException {
295         throw new BookException(JSOtherMsg.lookupText("This Book is read-only."));
296     }
297 
298     /* (non-Javadoc)
299      * @see org.crosswire.jsword.book.Book#setAliasKey(org.crosswire.jsword.passage.Key, org.crosswire.jsword.passage.Key)
300      */
301     public void setAliasKey(Key alias, Key source) throws BookException {
302         getBackend().setAliasKey(alias, source);
303     }
304 
305     /* (non-Javadoc)
306      * @see org.crosswire.jsword.book.basic.AbstractPassageBook#getFilter()
307      */
308     @Override
309     protected SourceFilter getFilter() {
310         return filter;
311     }
312 
313     /**
314      * The filter to use to convert to OSIS.
315      */
316     private SourceFilter filter;
317 
318     /**
319      * A cached representation of the global key list.
320      */
321     private Key global;
322 
323     /**
324      * The log stream
325      */
326     private static final Logger log = LoggerFactory.getLogger(SwordBook.class);
327 }
328