package net.bible.service.sword; import android.content.SharedPreferences; import android.util.Log; import net.bible.android.BibleApplication; import net.bible.android.activity.R; import net.bible.android.control.ApplicationScope; import net.bible.android.control.bookmark.BookmarkStyle; import net.bible.service.common.CommonUtils; import net.bible.service.common.Constants; import net.bible.service.common.Logger; import net.bible.service.common.ParseException; import net.bible.service.css.CssControl; import net.bible.service.font.FontControl; import net.bible.service.format.HtmlMessageFormatter; import net.bible.service.format.Note; import net.bible.service.format.OSISInputStream; import net.bible.service.format.osistohtml.OsisToHtmlParameters; import net.bible.service.format.osistohtml.osishandlers.OsisToCanonicalTextSaxHandler; import net.bible.service.format.osistohtml.osishandlers.OsisToHtmlSaxHandler; import net.bible.service.format.osistohtml.osishandlers.OsisToSpeakTextSaxHandler; import net.bible.service.format.usermarks.BookmarkFormatSupport; import net.bible.service.format.usermarks.MyNoteFormatSupport; import org.crosswire.common.xml.SAXEventProvider; import org.crosswire.jsword.book.Book; import org.crosswire.jsword.book.BookCategory; import org.crosswire.jsword.book.BookData; import org.crosswire.jsword.book.BookException; import org.crosswire.jsword.book.BookMetaData; import org.crosswire.jsword.book.Books; import org.crosswire.jsword.book.FeatureType; import org.crosswire.jsword.book.basic.AbstractPassageBook; import org.crosswire.jsword.passage.Key; import org.crosswire.jsword.passage.NoSuchKeyException; import org.crosswire.jsword.passage.Passage; import org.xml.sax.ContentHandler; import java.io.InputStream; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import javax.inject.Inject; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; /** JSword facade * * @author Martin Denham [mjdenham at gmail dot com] * @see gnu.lgpl.License for license details.<br> * The copyright to this program is held by it's author. */ @ApplicationScope public class SwordContentFacade { private DocumentParseMethod documentParseMethod = new DocumentParseMethod(); private final BookmarkFormatSupport bookmarkFormatSupport; private final MyNoteFormatSupport myNoteFormatSupport; private CssControl cssControl = new CssControl(); private static final String TAG = "SwordContentFacade"; private static SwordContentFacade singleton; // set to false for testing public static boolean isAndroid = true; //CommonUtils.isAndroid(); private static final Logger log = new Logger(SwordContentFacade.class.getName()); @Inject public SwordContentFacade(BookmarkFormatSupport bookmarkFormatSupport, MyNoteFormatSupport myNoteFormatSupport) { this.bookmarkFormatSupport = bookmarkFormatSupport; this.myNoteFormatSupport = myNoteFormatSupport; } /** top level method to fetch html from the raw document data */ public String readHtmlText(Book book, Key key) throws ParseException { String retVal = ""; if (book==null || key==null) { retVal = ""; } else if (Books.installed().getBook(book.getInitials())==null) { Log.w(TAG, "Book may have been uninstalled:"+book); String errorMsg = BibleApplication.getApplication().getString(R.string.document_not_installed, book.getInitials()); String htmlMsg = HtmlMessageFormatter.format(errorMsg); retVal = htmlMsg; } else if (!bookContainsAnyOf(book, key)) { Log.w(TAG, "KEY:"+key.getOsisID()+" not found in doc:"+book); String htmlMsg = HtmlMessageFormatter.format(R.string.error_key_not_in_document); retVal = htmlMsg; } else { // we have a fast way of handling OSIS zText docs but some docs need the superior JSword error recovery for mismatching tags // try to parse using optimised method first if a suitable document and it has not failed previously boolean isParsedOk = false; if ("OSIS".equals(book.getBookMetaData().getProperty("SourceType")) && "zText".equals(book.getBookMetaData().getProperty("ModDrv")) && documentParseMethod.isFastParseOkay(book, key)) { try { retVal = readHtmlTextOptimizedZTextOsis(book, key); isParsedOk = true; } catch (ParseException pe) { documentParseMethod.failedToParse(book, key); } } // fall back to slightly slower JSword method with JSword's fallback approach of removing all tags if (!isParsedOk) { retVal = readHtmlTextStandardJSwordMethod(book, key); } } return retVal; } /** Get Footnotes and references from specified document page */ public List<Note> readFootnotesAndReferences(Book book, Key key) throws ParseException { List<Note> retVal = new ArrayList<>(); try { // based on standard JSword SAX handling method because few performance gains would be gained for the extra complexity of Streaming BookData data = new BookData(book, key); SAXEventProvider osissep = data.getSAXEventProvider(); if (osissep != null) { OsisToHtmlSaxHandler osisToHtml = getSaxHandler(book, key); osissep.provideSAXEvents(osisToHtml); retVal = osisToHtml.getNotesList(); } else { Log.e(TAG, "No osis SEP returned"); } return retVal; } catch (Exception e) { log.error("Parsing error", e); throw new ParseException("Parsing error", e); } } /** * Use OSISInputStream which loads a single verse at a time as required. * This reduces memory requirements compared to standard JDom SaxEventProvider */ private String readHtmlTextOptimizedZTextOsis(Book book, Key key) throws ParseException { log.debug("Using fast method to fetch document data"); /** * When you supply an InputStream, the SAX implementation wraps the stream in an InputStreamReader; * then SAX automatically detects the correct character encoding from the stream. You can then omit the setEncoding() step, * reducing the method invocations once again. The result is an application that is faster, and always has the correct character encoding. */ InputStream is = new OSISInputStream(book, key); OsisToHtmlSaxHandler osisToHtml = getSaxHandler(book, key); SAXParser parser = getSAXParser(); try { parser.parse(is, osisToHtml); } catch (Exception e) { log.error("Parsing error", e); throw new ParseException("Parsing error", e); } return osisToHtml.toString(); } private String readHtmlTextStandardJSwordMethod(Book book, Key key) throws ParseException { log.debug("Using standard JSword to fetch document data"); String retVal; try { BookData data = new BookData(book, key); SAXEventProvider osissep = data.getSAXEventProvider(); if (osissep == null) { Log.e(TAG, "No osis SEP returned"); retVal = "Error fetching osis SEP"; } else { OsisToHtmlSaxHandler osisToHtml = getSaxHandler(book, key); osissep.provideSAXEvents(osisToHtml); retVal = osisToHtml.toString(); } return retVal; } catch (Exception e) { log.error("Parsing error", e); throw new ParseException("Parsing error", e); } } /** * Obtain a SAX event provider for the OSIS document representation of one * or more book entries. * * @param book * the book to use * @param reference * a reference, appropriate for the book, of one or more entries */ public SAXEventProvider getOSIS(Book book, String reference, int maxKeyCount) throws BookException, NoSuchKeyException { Key key; if (BookCategory.BIBLE.equals(book.getBookCategory())) { key = book.getKey(reference); ((Passage) key).trimVerses(maxKeyCount); } else { key = book.createEmptyKeyList(); Iterator<Key> iter = book.getKey(reference).iterator(); int count = 0; while (iter.hasNext()) { if (++count >= maxKeyCount) { break; } key.addAll(iter.next()); } } BookData data = new BookData(book, key); return data.getSAXEventProvider(); } /** * Get just the canonical text of one or more book entries without any * markup. * * @param book * the book to use * @param key * a reference, appropriate for the book, of one or more entries */ public String getCanonicalText(Book book, Key key) throws NoSuchKeyException, BookException, ParseException { try { BookData data = new BookData(book, key); SAXEventProvider osissep = data.getSAXEventProvider(); ContentHandler osisHandler = new OsisToCanonicalTextSaxHandler(); osissep.provideSAXEvents(osisHandler); return osisHandler.toString(); } catch (Exception e) { Log.e(TAG, "Error getting text from book" , e); return BibleApplication.getApplication().getString(R.string.error_occurred); } } /** * Get text to be spoken without any markup. * * @param book * the book to use * @param key * a reference, appropriate for the book, of one or more entries */ public String getTextToSpeak(Book book, Key key) throws NoSuchKeyException, BookException, ParseException { try { BookData data = new BookData(book, key); SAXEventProvider osissep = data.getSAXEventProvider(); boolean sayReferences = BookCategory.GENERAL_BOOK.equals(book.getBookCategory()); ContentHandler osisHandler = new OsisToSpeakTextSaxHandler(sayReferences); osissep.provideSAXEvents(osisHandler); return osisHandler.toString(); } catch (Exception e) { Log.e(TAG, "Error getting text from book" , e); return BibleApplication.getApplication().getString(R.string.error_occurred); } } private SAXParser saxParser; private SAXParser getSAXParser() throws ParseException { try { if (saxParser==null) { SAXParserFactory spf = SAXParserFactory.newInstance(); spf.setValidating(false); saxParser = spf.newSAXParser(); } } catch (Exception e) { log.error("SAX parser error", e); throw new ParseException("SAX parser error", e); } return saxParser; } /** * Get just the canonical text of one or more book entries without any * markup. * * @param book * the book to use * @param reference * a reference, appropriate for the book, of one or more entries */ public String getPlainText(Book book, String reference, int maxKeyCount) throws BookException, NoSuchKeyException { String plainText = ""; try { if (book != null) { Key key = book.getKey(reference); plainText = getPlainText(book, key, maxKeyCount); } } catch (Exception e) { Log.e(TAG, "Error getting plain text", e); } return plainText; } /** * Get just the canonical text of one or more book entries without any * markup. * * @param book * the book to use * @param key * a reference, appropriate for the book, of one or more entries */ public String getPlainText(Book book, Key key, int maxKeyCount) throws BookException, NoSuchKeyException { String plainText = ""; try { if (book != null) { plainText = getCanonicalText(book, key); // trim any preceeding spaces that make the final output look uneven plainText = plainText.trim(); } } catch (Exception e) { Log.e(TAG, "Error getting plain text", e); } return plainText; } public Key search(Book bible, String searchText) throws BookException { // example of fetching Strongs ref - only works with downloaded indexes! // Book book = getDocumentByInitials("KJV"); // Key key1 = book.find("strong:h3068"); // System.out.println("h3068 result count:"+key1.getCardinality()); Log.d(TAG, "Searching:"+bible+" Search term:" + searchText); // This does a standard operator search. See the search // documentation for more examples of how to search Key key = bible.find(searchText); //$NON-NLS-1$ Log.d(TAG, "There are "+key.getCardinality()+" verses containing " + searchText); return key; } private OsisToHtmlSaxHandler getSaxHandler(Book book, Key key) { OsisToHtmlParameters osisToHtmlParameters = new OsisToHtmlParameters(); BookCategory bookCategory = book.getBookCategory(); BookMetaData bmd = book.getBookMetaData(); osisToHtmlParameters.setLeftToRight(bmd.isLeftToRight()); osisToHtmlParameters.setLanguageCode(book.getLanguage().getCode()); osisToHtmlParameters.setModuleBasePath(book.getBookMetaData().getLocation()); // If Bible or Commentary then set Basis for partial references to current Key/Verse if (BookCategory.BIBLE.equals(bookCategory) || BookCategory.COMMENTARY.equals(bookCategory)) { osisToHtmlParameters.setBasisRef(key); osisToHtmlParameters.setDocumentVersification(((AbstractPassageBook)book).getVersification()); } if (isAndroid) { // HunUj has an error in that refs are not wrapped so automatically add notes around refs osisToHtmlParameters.setAutoWrapUnwrappedRefsInNote("HunUj".equals(book.getInitials())); SharedPreferences preferences = CommonUtils.getSharedPreferences(); if (preferences!=null) { // prefs applying to any doc type osisToHtmlParameters.setShowNotes(preferences.getBoolean("show_notes_pref", false)); osisToHtmlParameters.setRedLetter(preferences.getBoolean("red_letter_pref", false)); osisToHtmlParameters.setCssStylesheetList( cssControl.getAllStylesheetLinks() ); // show verse numbers if user has selected to show verse numbers AND the book is a bible (so don't even try to show verses in a Dictionary) if (BookCategory.BIBLE.equals(bookCategory)) { osisToHtmlParameters.setShowVerseNumbers(preferences.getBoolean("show_verseno_pref", true) && BookCategory.BIBLE.equals(bookCategory)); osisToHtmlParameters.setVersePerline(preferences.getBoolean("verse_per_line_pref", false)); osisToHtmlParameters.setShowMyNotes(preferences.getBoolean("show_mynotes_pref", true)); osisToHtmlParameters.setShowBookmarks(preferences.getBoolean("show_bookmarks_pref", true)); osisToHtmlParameters.setDefaultBookmarkStyle(BookmarkStyle.valueOf(preferences.getString("default_bookmark_style_pref", BookmarkStyle.YELLOW_STAR.name()))); osisToHtmlParameters.setShowTitles(preferences.getBoolean("section_title_pref", true)); osisToHtmlParameters.setVersesWithNotes(myNoteFormatSupport.getVersesWithNotesInPassage(key)); osisToHtmlParameters.setBookmarkStylesByBookmarkedVerse(bookmarkFormatSupport.getVerseBookmarkStylesInPassage(key)); // showMorphology depends on showStrongs to allow the toolbar toggle button to affect both strongs and morphology boolean showStrongs = preferences.getBoolean("show_strongs_pref", true); osisToHtmlParameters.setShowStrongs(showStrongs); osisToHtmlParameters.setShowMorphology(showStrongs && preferences.getBoolean("show_morphology_pref", false)); } if (BookCategory.DICTIONARY.equals(bookCategory)) { if (book.hasFeature(FeatureType.HEBREW_DEFINITIONS)) { //add allHebrew refs link String prompt = BibleApplication.getApplication().getString(R.string.all_hebrew_occurrences); osisToHtmlParameters.setExtraFooter("<br /><a href='"+Constants.ALL_HEBREW_OCCURRENCES_PROTOCOL+":"+key.getName()+"' class='allStrongsRefsLink'>"+prompt+"</a>"); //convert text refs to links osisToHtmlParameters.setConvertStrongsRefsToLinks(true); } else if (book.hasFeature(FeatureType.GREEK_DEFINITIONS)) { //add allGreek refs link String prompt = BibleApplication.getApplication().getString(R.string.all_greek_occurrences); osisToHtmlParameters.setExtraFooter("<br /><a href='"+Constants.ALL_GREEK_OCCURRENCES_PROTOCOL+":"+key.getName()+"' class='allStrongsRefsLink'>"+prompt+"</a>"); //convert text refs to links osisToHtmlParameters.setConvertStrongsRefsToLinks(true); } } // which font, if any osisToHtmlParameters.setFont(FontControl.getInstance().getFontForBook(book)); osisToHtmlParameters.setCssClassForCustomFont(FontControl.getInstance().getCssClassForCustomFont(book)); // indent depth - larger screens have a greater indent osisToHtmlParameters.setIndentDepth(CommonUtils.getResourceInteger(R.integer.poetry_indent_chars)); } } return new OsisToHtmlSaxHandler(osisToHtmlParameters); } public static void setAndroid(boolean isAndroid) { SwordContentFacade.isAndroid = isAndroid; } /** * When checking a book contains a chapter SwordBook returns false if verse 0 is not in the chapter so this method compensates for that * * This can be removed if SwordBook.contains is converted to be containsAnyOf as discussed in JS-273 */ private boolean bookContainsAnyOf(Book book, Key key) { if (book.contains(key)) { return true; } for (Key aKey : key) { if (book.contains(aKey)) { return true; } } return false; } }