package com.tyndalehouse.step.core.service.jsword.impl;
import static com.tyndalehouse.step.core.utils.StringUtils.isBlank;
import static com.tyndalehouse.step.core.utils.StringUtils.isNotEmpty;
import static org.crosswire.jsword.book.BookCategory.BIBLE;
import java.util.*;
import javax.inject.Inject;
import com.tyndalehouse.step.core.exceptions.StepInternalException;
import com.tyndalehouse.step.core.models.InterlinearMode;
import com.tyndalehouse.step.core.service.helpers.VersionResolver;
import com.tyndalehouse.step.core.utils.JSwordUtils;
import com.tyndalehouse.step.core.utils.StringUtils;
import org.crosswire.jsword.book.Book;
import org.crosswire.jsword.book.FeatureType;
import org.crosswire.jsword.book.basic.AbstractPassageBook;
import org.crosswire.jsword.versification.BibleBook;
import org.crosswire.jsword.versification.DivisionName;
import org.crosswire.jsword.versification.Versification;
import org.crosswire.jsword.versification.system.Versifications;
import com.tyndalehouse.step.core.models.BookName;
import com.tyndalehouse.step.core.models.LookupOption;
import com.tyndalehouse.step.core.service.jsword.JSwordMetadataService;
import com.tyndalehouse.step.core.service.jsword.JSwordVersificationService;
/**
* Provides metadata for JSword modules
*
* @author chrisburrell
*/
public class JSwordMetadataServiceImpl implements JSwordMetadataService {
private static final String BOOK_CHAPTER_FORMAT = "%s %d";
private final JSwordVersificationService versificationService;
private final VersionResolver versionResolver;
/**
* Sets up the service for providing metadata information
*
* @param versificationService the versification service
*/
@Inject
public JSwordMetadataServiceImpl(final JSwordVersificationService versificationService, final VersionResolver versionResolver) {
this.versificationService = versificationService;
this.versionResolver = versionResolver;
}
@Override
public String getFirstChapterReference(final String version) {
final Book bookFromVersion = versificationService.getBookFromVersion(version);
if(bookFromVersion instanceof AbstractPassageBook) {
final Iterator<BibleBook> bookIterator = ((AbstractPassageBook) bookFromVersion).getBibleBooks().iterator();
BibleBook bibleBook = bookIterator.next();
if(BibleBook.INTRO_BIBLE.equals(bibleBook) || BibleBook.INTRO_OT.equals(bibleBook) || BibleBook.INTRO_NT.equals(bibleBook)) {
bibleBook = bookIterator.next();
if(BibleBook.INTRO_OT.equals(bibleBook) || BibleBook.INTRO_NT.equals(bibleBook)) {
bibleBook = bookIterator.next();
}
}
return String.format("%s.%d", bibleBook.getOSIS(), 1);
}
throw new StepInternalException("Unable to ascertain first chapter of book.");
}
@Override
public Set<LookupOption> getFeatures(final String version, List<String> extraVersions) {
// obtain the book
final Book book = this.versificationService.getBookFromVersion(version);
final Set<LookupOption> options = new HashSet<LookupOption>(LookupOption.values().length * 2);
if (book == null) {
return options;
}
// some options are always there for Bibles:
addBibleCategoryOptions(book, options);
addRedLetterOptions(book, options);
addStrongNumberOptions(book, options);
addMorphologyOptions(book, options);
addNotesOptions(book, options);
addHebrewOptions(book, options);
addAncientOptions(version, extraVersions, options);
addMasterAncientOptions(book, options);
addAllMatchingLookupOptions(book, options);
addHiddenOptions(options);
return options;
}
private void addMasterAncientOptions(final Book currentVersion, final Set<LookupOption> options) {
if (JSwordUtils.isAncientGreekBook(currentVersion) || JSwordUtils.isAncientHebrewBook(currentVersion)) {
options.add(LookupOption.TRANSLITERATE_ORIGINAL);
}
}
private void addHiddenOptions(final Set<LookupOption> options) {
options.add(LookupOption.HIDE_XGEN);
options.add(LookupOption.CHAPTER_BOOK_VERSE_NUMBER);
options.add(LookupOption.HEADINGS_ONLY);
options.add(LookupOption.HIDE_COMPARE_HEADERS);
}
/**
* Adds options that apply regardless of the conditions
*
* @param currentVersion the current primary version
* @param extraVersions the secondary versions that affect feature resolution
* @param options the set of options
*/
private void addAncientOptions(final String currentVersion, final List<String> extraVersions, final Set<LookupOption> options) {
final List<String> allVersions = new ArrayList<String>(extraVersions);
allVersions.add(currentVersion);
boolean hasGreekVersion, hasHebrewVersion = hasGreekVersion = false;
for (String version : allVersions) {
Book book = this.versificationService.getBookFromVersion(version);
if (JSwordUtils.isAncientGreekBook(book)) {
hasGreekVersion = true;
}
if (JSwordUtils.isAncientHebrewBook(book)) {
hasHebrewVersion = true;
}
}
//hebrew/greek options for interlinears
if (hasGreekVersion) {
options.add(LookupOption.GREEK_ACCENTS);
}
if (hasHebrewVersion) {
options.add(LookupOption.HEBREW_ACCENTS);
options.add(LookupOption.HEBREW_VOWELS);
}
}
/**
* For Hebrew books, we hard code availability of seg divisions for OHB and WLC
*
* @param book the Book in question
* @param options the available options
*/
private void addHebrewOptions(final Book book, final Set<LookupOption> options) {
if ("OSMHB".equals(book.getInitials()) || "OHB".equals(book.getInitials()) || "OSHB".equals(book.getInitials())
|| "WLC".equals(book.getInitials())) {
options.add(LookupOption.DIVIDE_HEBREW);
}
}
/**
* Add all options when the options match by their XsltParameter Name
*
* @param book the book
* @param options the options to be added to
*/
private void addAllMatchingLookupOptions(final Book book, final Set<LookupOption> options) {
// cycle through each option
for (final LookupOption lo : LookupOption.values()) {
final FeatureType ft = FeatureType.fromString(lo.getXsltParameterName());
if (ft != null && isNotEmpty(lo.name()) && book.getBookMetaData().hasFeature(ft)) {
options.add(lo);
}
}
}
/**
* Adds options for notes
*
* @param book the book
* @param options the options to be added to
*/
private void addNotesOptions(final Book book, final Set<LookupOption> options) {
if (book.getBookMetaData().hasFeature(FeatureType.FOOTNOTES)
|| book.getBookMetaData().hasFeature(FeatureType.SCRIPTURE_REFERENCES)) {
options.add(LookupOption.NOTES);
}
}
/**
* Adds options for morphology
*
* @param book the book
* @param options the options to be added to
*/
private void addMorphologyOptions(final Book book, final Set<LookupOption> options) {
if (book.hasFeature(FeatureType.MORPHOLOGY)) {
options.add(LookupOption.COLOUR_CODE);
}
}
/**
* Adds options for strong numbers
*
* @param book the book
* @param options the options to be added to
*/
private void addStrongNumberOptions(final Book book, final Set<LookupOption> options) {
if (book.getBookMetaData().hasFeature(FeatureType.STRONGS_NUMBERS)) {
options.add(LookupOption.ENGLISH_VOCAB);
options.add(LookupOption.GREEK_VOCAB);
options.add(LookupOption.TRANSLITERATION);
options.add(LookupOption.INTERLINEAR);
}
}
/**
* Adds options for red letter Bible
*
* @param book the book
* @param options the options to be added to
*/
private void addRedLetterOptions(final Book book, final Set<LookupOption> options) {
if (book.getBookMetaData().hasFeature(FeatureType.WORDS_OF_CHRIST)) {
options.add(LookupOption.RED_LETTER);
}
}
/**
* Adds options if module is a Bible
*
* @param book the book
* @param options the options to be added to
*/
private void addBibleCategoryOptions(final Book book, final Set<LookupOption> options) {
if (BIBLE.equals(book.getBookCategory())) {
options.add(LookupOption.VERSE_NUMBERS);
options.add(LookupOption.VERSE_NEW_LINE);
}
}
@Override
public List<BookName> getBibleBookNames(final String bookStart, final String version, final String bookScope) {
return this.getBibleBookNames(bookStart, version, bookScope, false);
}
@Override
public List<BookName> getBibleBookNames(final String bookStart, final String version, boolean autoLookupSingleBooks) {
return this.getBibleBookNames(bookStart, version, null, autoLookupSingleBooks);
}
/**
* returns a list of matching names or references in a particular book
*
* @param bookStart the name of the matching key to look across book names
* @param version the name of the version, defaults to ESV if not found
* @param bookScope a scope that reduces the search
* @param autoLookupSingleBooks true to indicate a single book should resolve to chapters
* @return a list of matching bible book names
*/
private List<BookName> getBibleBookNames(final String bookStart, final String version, final String bookScope,
boolean autoLookupSingleBooks) {
final String lookup = isBlank(bookStart) ? "" : bookStart;
final Versification versification = this.versificationService.getVersificationForVersion(version);
final List<BookName> books = getBooks(lookup, versification, bookScope, autoLookupSingleBooks);
if (books.isEmpty()) {
return getBooks(lookup, Versifications.instance().getVersification(Versifications.DEFAULT_V11N), bookScope, autoLookupSingleBooks);
}
return books;
}
/**
* Looks through a versification for a particular type of book
*
* @param bookStart the string to match
* @param versification the versification we are interested in
* @param bookScope the actual book required, usually to get chapters
* @param autoLookupSingleBooks autoLookupSingleBooks true to indicate that for a single book, we should lookup
* the chapters inside
* @return the list of matching names
*/
private List<BookName> getBooks(final String bookStart, final Versification versification, final String bookScope,
final boolean autoLookupSingleBooks) {
final String searchPattern = bookStart.toLowerCase(Locale.getDefault()).trim();
final List<BookName> matchingNames = new ArrayList<BookName>();
final Iterator<BibleBook> bookIterator = versification.getBookIterator();
if (StringUtils.isNotBlank(bookScope)) {
final List<BookName> optionsInBook = getChapters(versification, versification.getBook(bookScope));
return optionsInBook;
}
BibleBook b = null;
while (bookIterator.hasNext()) {
final BibleBook book = bookIterator.next();
if (versification.getLongName(book).toLowerCase().startsWith(searchPattern)
|| versification.getPreferredName(book).toLowerCase().startsWith(searchPattern)
|| versification.getShortName(book).toLowerCase().startsWith(searchPattern)) {
b = book;
addBookName(matchingNames, book, versification);
}
}
if (autoLookupSingleBooks && matchingNames.size() == 1) {
final List<BookName> optionsInBook = getChapters(versification, b);
if (!optionsInBook.isEmpty()) {
return optionsInBook;
}
}
return matchingNames;
}
/**
* Adds all Bible books except for INTROs to NT, OT and Bible.
*
* @param matchingNames the list of current names
* @param bookName the book that we are examining
* @param versification the versification attached to the book.
*/
private void addBookName(final List<BookName> matchingNames, final BibleBook bookName,
final Versification versification) {
if (BibleBook.INTRO_BIBLE.equals(bookName) || BibleBook.INTRO_NT.equals(bookName)
|| BibleBook.INTRO_OT.equals(bookName)) {
return;
}
matchingNames.add(new BookName(versification.getShortName(bookName), versification
.getLongName(bookName),
DivisionName.BIBLE.contains(bookName) ? BookName.Section.BIBLE_BOOK : BookName.Section.APOCRYPHA,
versification.getLastChapter(bookName) != 1, bookName.getOSIS()));
}
/**
* Returns the list of chapters
*
* @param versification the versification
* @param book the book
* @return a list of books
*/
private List<BookName> getChapters(final Versification versification, final BibleBook book) {
final int lastChapter = versification.getLastChapter(book);
final List<BookName> chapters = new ArrayList<BookName>();
//we add the whole book + all the chapters
BookName.Section section = DivisionName.BIBLE.contains(book) ? BookName.Section.BIBLE_BOOK : BookName.Section.APOCRYPHA;
chapters.add(new BookName(versification.getShortName(book), versification
.getLongName(book), section, versification.getLastChapter(book) != 1, book, false, book.getOSIS()));
for (int ii = 1; ii <= lastChapter; ii++) {
// final char f = Character.toUpperCase(searchSoFar.charAt(0));
// make sure first letter is CAPS, followed by the rest of the word and the chapter number
final String chapNumber = String
.format(BOOK_CHAPTER_FORMAT, versification.getShortName(book), ii);
final String longChapNumber = String.format(BOOK_CHAPTER_FORMAT, versification.getLongName(book),
ii);
chapters.add(new BookName(chapNumber, longChapNumber, BookName.Section.PASSAGE, false, book, true, JSwordUtils.getChapterOsis(book, ii)));
}
return chapters;
}
@Override
public boolean hasVocab(final String version) {
return supportsStrongs(this.versificationService.getBookFromVersion(version));
}
@Override
public boolean supportsStrongs(Book book) {
return book.hasFeature(FeatureType.STRONGS_NUMBERS);
}
@Override
public String[] getLanguages(final String... versions) {
String[] languages = new String[versions.length];
for (int i = 0; i < versions.length; i++) {
final String version = versions[i];
Book b = this.versificationService.getBookFromVersion(version);
languages[i] = b.getLanguage().getCode();
}
return languages;
}
@Override
public InterlinearMode getBestInterlinearMode(String version, List<String> extraVersions, final InterlinearMode interlinearMode) {
if (extraVersions == null || extraVersions.size() == 0) {
return InterlinearMode.NONE;
}
//we've at least got several versions here, so, we prefer the option given to defaults
if (interlinearMode == InterlinearMode.INTERLEAVED || interlinearMode == InterlinearMode.COLUMN) {
return interlinearMode;
}
//so we've either asked for nothing, or asked for something that we need to check is appropriate
Book main = this.versificationService.getBookFromVersion(version);
String firstLanguage = main.getLanguage().getCode();
boolean supportsStrongs = this.supportsStrongs(main);
boolean sameLanguageAndBible = main.getBookCategory() == BIBLE;
for (String extraVersion : extraVersions) {
Book b = this.versificationService.getBookFromVersion(extraVersion);
if (supportsStrongs && !this.supportsStrongs(b)) {
supportsStrongs = false;
}
if (!firstLanguage.equalsIgnoreCase(b.getLanguage().getCode()) || b.getBookCategory() != BIBLE) {
sameLanguageAndBible = false;
}
//small optimization
if (!supportsStrongs && !sameLanguageAndBible) {
break;
}
}
//if compare options were given and are available, we return these.
if (interlinearMode == InterlinearMode.INTERLEAVED_COMPARE || interlinearMode == InterlinearMode.COLUMN_COMPARE) {
return getSameOrDowngradedInterlinearMode(interlinearMode, sameLanguageAndBible);
}
if (interlinearMode == InterlinearMode.INTERLINEAR && supportsStrongs && allVersionsSameTagging(version, extraVersions)) {
return InterlinearMode.INTERLINEAR;
}
return InterlinearMode.INTERLEAVED;
}
/**
* We check that all versions have the same Greek/Hebrew tagging. For example, Septuagint tagged texts should
* not be mapped to the Hebrew texts
* @param version the version
* @param extraVersions the extra versions
* @return true if all versions are of the same kind
*/
private boolean allVersionsSameTagging(final String version, final List<String> extraVersions) {
final boolean isSeptuagint = this.versionResolver.isSeptuagintTagging(version);
for(String v : extraVersions) {
if(isSeptuagint != this.versionResolver.isSeptuagintTagging(v)) {
return false;
}
}
return true;
}
@Override
public boolean supportsFeature(final String version, LookupOption... options) {
Book b = this.versificationService.getBookFromVersion(version);
for(LookupOption lo : options) {
FeatureType ft = lo.getFeature();
if(ft != null) {
if(!b.getBookMetaData().hasFeature(ft)) {
return false;
}
}
}
return true;
}
/**
* if all versions are of the same language, then we return the interlinear mode.
* Otherwise we return INTERLEAVED if INTERLEAVED_COMPARE was given, and COLUMN if COLUMN_COMPARED was given
*
* @param interlinearMode the original interlinear mode
* @param sameLanguage true to indicate all versions are of the same language
* @return the final interlinear mode to be used going forward.
*/
private InterlinearMode getSameOrDowngradedInterlinearMode(final InterlinearMode interlinearMode, final boolean sameLanguage) {
if (sameLanguage) {
return interlinearMode;
} else if (interlinearMode == InterlinearMode.INTERLEAVED_COMPARE) {
return InterlinearMode.INTERLEAVED;
} else {
return InterlinearMode.COLUMN;
}
}
}