package yuku.alkitab.yes2; import android.util.Log; import yuku.alkitab.io.BibleReader; import yuku.alkitab.model.Book; import yuku.alkitab.model.FootnoteEntry; import yuku.alkitab.model.PericopeBlock; import yuku.alkitab.model.SingleChapterVerses; import yuku.alkitab.model.XrefEntry; import yuku.alkitab.util.Ari; import yuku.alkitab.yes2.compress.SnappyInputStream; import yuku.alkitab.yes2.io.RandomAccessFileRandomInputStream; import yuku.alkitab.yes2.io.RandomInputStream; import yuku.alkitab.yes2.io.Yes2VerseTextDecoder; import yuku.alkitab.yes2.model.SectionIndex; import yuku.alkitab.yes2.model.Yes2Book; import yuku.alkitab.yes2.section.BooksInfoSection; import yuku.alkitab.yes2.section.FootnotesSection; import yuku.alkitab.yes2.section.PericopesSection; import yuku.alkitab.yes2.section.TextSection; import yuku.alkitab.yes2.section.VersionInfoSection; import yuku.alkitab.yes2.section.XrefsSection; import yuku.bintex.BintexReader; import yuku.bintex.ValueMap; import java.io.IOException; import java.util.Arrays; import java.util.List; public class Yes2Reader implements BibleReader { private static final String TAG = Yes2Reader.class.getSimpleName(); private RandomAccessFileRandomInputStream file_; private SectionIndex sectionIndex_; // cached in memory private VersionInfoSection versionInfo_; private PericopesSection pericopesSection_; private TextSectionReader textSectionReader_; private XrefsSection xrefsSection_; private FootnotesSection footnotesSection_; static class Yes2SingleChapterVerses extends SingleChapterVerses { private final String[] verses; public Yes2SingleChapterVerses(String[] verses) { this.verses = verses; } @Override public String getVerse(int verse_0) { return verses[verse_0]; } @Override public int getVerseCount() { return verses.length; } } /** * This class simplify many operations regarding reading the verse texts from the yes file. * This stores the offset to the beginning of text section content * and also understands the text section attributes (compression, encryption etc.) */ static class TextSectionReader { private final RandomAccessFileRandomInputStream file_; private final Yes2VerseTextDecoder decoder_; private final long sectionContentOffset_; private BintexReader br_; private SnappyInputStream snappyInputStream; // null means no compression public TextSectionReader(RandomAccessFileRandomInputStream file, Yes2VerseTextDecoder decoder, ValueMap sectionAttributes, long sectionContentOffset) throws Exception { file_ = file; decoder_ = decoder; sectionContentOffset_ = sectionContentOffset; br_ = new BintexReader(null); if (sectionAttributes != null) { String compressionName = sectionAttributes.getString("compression.name"); if (compressionName != null) { if ("snappy-blocks".equals(compressionName)) { snappyInputStream = SnappyInputStream.getInstanceFromAttributes(file_, sectionAttributes, sectionContentOffset); } else { throw new Exception("Compression " + compressionName + " is not supported"); } } } } public Yes2SingleChapterVerses loadVerseText(Yes2Book yes2Book, int chapter_1, boolean dontSeparateVerses, boolean lowercase) throws Exception { int contentOffset = yes2Book.offset; contentOffset += yes2Book.chapter_offsets[chapter_1 - 1]; BintexReader br; if (snappyInputStream != null) { snappyInputStream.seek(contentOffset); br = br_.reuse(snappyInputStream); } else { file_.seek(sectionContentOffset_ + contentOffset); br = br_.reuse(file_); } int verse_count = yes2Book.verse_counts[chapter_1 - 1]; if (dontSeparateVerses) { return new Yes2SingleChapterVerses(new String[] { decoder_.makeIntoSingleString(br, verse_count, lowercase), }); } else { return new Yes2SingleChapterVerses(decoder_.separateIntoVerses(br, verse_count, lowercase)); } } } public Yes2Reader(RandomAccessFileRandomInputStream input) { this.file_ = input; } /** Read section index */ private synchronized void loadSectionIndex() throws IOException { if (sectionIndex_ != null) { // we have read it previously. return; } file_.seek(0); { // check header byte[] buf = new byte[8]; file_.read(buf); if (!Arrays.equals(buf, new byte[] { (byte) 0x98, 0x58, 0x0d, 0x0a, 0x00, 0x5d, (byte) 0xe0, 0x02 /* yes version 2 */})) { throw new RuntimeException("YES2: Header is incorrect. Found: " + Arrays.toString(buf)); } } file_.seek(12); // start of sectionIndex sectionIndex_ = SectionIndex.read(file_); } private synchronized void loadVersionInfo() throws Exception { if (seekToSection("versionInfo")) { versionInfo_ = new VersionInfoSection.Reader().read(file_); } } private synchronized boolean seekToSection(String sectionName) throws IOException { loadSectionIndex(); if (sectionIndex_ == null) { Log.e(TAG, "@@seekToSection Could not load section index"); return false; } if (sectionIndex_.seekToSectionContent(sectionName, file_)) { return true; } else { Log.e(TAG, "@@seekToSection Could not seek to section: " + sectionName); return false; } } @Override public String getLocale() { try { loadVersionInfo(); return versionInfo_.locale; } catch (Exception e) { Log.e(TAG, "yes load version info error", e); return ""; } } @Override public String getShortName() { try { loadVersionInfo(); return versionInfo_.shortName; } catch (Exception e) { Log.e(TAG, "yes load version info error", e); return ""; } } @Override public String getLongName() { try { loadVersionInfo(); return versionInfo_.longName; } catch (Exception e) { Log.e(TAG, "yes load version info error", e); return ""; } } @Override public String getDescription() { try { loadVersionInfo(); return versionInfo_.description; } catch (Exception e) { Log.e(TAG, "yes load version info error", e); return ""; } } @Override public Book[] loadBooks() { try { loadVersionInfo(); if (seekToSection(BooksInfoSection.SECTION_NAME)) { BooksInfoSection section = new BooksInfoSection.Reader().read(file_); List<Yes2Book> books = section.yes2Books; return books.toArray(new Yes2Book[books.size()]); } Log.e(TAG, "no section named " + BooksInfoSection.SECTION_NAME); return null; } catch (Exception e) { Log.e(TAG, "loadBooks error", e); return null; } } @Override public Yes2SingleChapterVerses loadVerseText(Book book, int chapter_1, boolean dontSeparateVerses, boolean lowercase) { Yes2Book yes2Book = (Yes2Book) book; try { if (chapter_1 <= 0 || chapter_1 > yes2Book.chapter_count) { return null; } if (textSectionReader_ == null) { // init text decoder Yes2VerseTextDecoder decoder; int textEncoding = versionInfo_.textEncoding; if (textEncoding == 1) { decoder = new Yes2VerseTextDecoder.Ascii(); } else if (textEncoding == 2) { decoder = new Yes2VerseTextDecoder.Utf8(); } else { Log.e(TAG, "Text encoding " + textEncoding + " not supported! Fallback to ascii."); decoder = new Yes2VerseTextDecoder.Ascii(); } ValueMap sectionAttributes = sectionIndex_.getSectionAttributes(TextSection.SECTION_NAME, file_); long sectionContentOffset = sectionIndex_.getAbsoluteOffsetForSectionContent(TextSection.SECTION_NAME); textSectionReader_ = new TextSectionReader(file_, decoder, sectionAttributes, sectionContentOffset); } return textSectionReader_.loadVerseText(yes2Book, chapter_1, dontSeparateVerses, lowercase); } catch (Exception e) { Log.e(TAG, "@@loadVerseText error book=" + book + " chapter_1=" + chapter_1 + " dontSeparateVerses=" + dontSeparateVerses + " lowercase=" + lowercase, e); return null; } } @Override public int loadPericope(int bookId, int chapter_1, int[] aris, PericopeBlock[] blocks, int max) { try { loadVersionInfo(); if (versionInfo_.hasPericopes == 0) { return 0; } if (pericopesSection_ == null) { // not yet loaded! final RandomInputStream sectionInput = prepareLoadSection(PericopesSection.SECTION_NAME); if (sectionInput == null) { return 0; } pericopesSection_ = new PericopesSection.Reader().read(sectionInput); } if (pericopesSection_ == null) { Log.e(TAG, "Didn't succeed in loading pericopes section"); return 0; } int ariMin = Ari.encode(bookId, chapter_1, 0); int ariMax = Ari.encode(bookId, chapter_1 + 1, 0); return pericopesSection_.getPericopesForAris(ariMin, ariMax, aris, blocks, max); } catch (Exception e) { Log.e(TAG, "General exception in loading pericope block", e); return 0; } } @Override public XrefEntry getXrefEntry(int arif) { if (xrefsSection_ == null) { // not yet loaded! try { final RandomInputStream sectionInput = prepareLoadSection(XrefsSection.SECTION_NAME); if (sectionInput == null) { return null; } xrefsSection_ = new XrefsSection.Reader().read(sectionInput); } catch (Exception e) { Log.e(TAG, "General exception in loading xref section", e); return null; } } return xrefsSection_.getXrefEntry(arif); } @Override public FootnoteEntry getFootnoteEntry(final int arif) { if (footnotesSection_ == null) { // not yet loaded! try { final RandomInputStream sectionInput = prepareLoadSection(FootnotesSection.SECTION_NAME); if (sectionInput == null) { return null; } footnotesSection_ = new FootnotesSection.Reader().read(sectionInput); } catch (Exception e) { Log.e(TAG, "General exception in loading footnote section", e); return null; } } return footnotesSection_.getFootnoteEntry(arif); } /** * Prepares an input stream for a section. * @return an input stream that transparently handles compressed/uncompressed data. Null if the section name is not found. */ RandomInputStream prepareLoadSection(final String sectionName) throws IOException { final ValueMap sectionAttributes = sectionIndex_.getSectionAttributes(sectionName, file_); final long sectionContentOffset = sectionIndex_.getAbsoluteOffsetForSectionContent(sectionName); RandomInputStream sectionInput = null; if (sectionAttributes != null) { String compressionName = sectionAttributes.getString("compression.name"); if (compressionName != null) { if ("snappy-blocks".equals(compressionName)) { sectionInput = SnappyInputStream.getInstanceFromAttributes(file_, sectionAttributes, sectionContentOffset); } else { throw new IOException("Compression " + compressionName + " is not supported"); } } } // no compression detected if (sectionInput == null) { sectionInput = file_; } if (seekToSection(sectionName)) { return sectionInput; } else { return null; } } }