package com.tyndalehouse.step.core.service.impl; import com.tyndalehouse.step.core.exceptions.StepInternalException; import com.tyndalehouse.step.core.models.OsisWrapper; import com.tyndalehouse.step.core.service.BibleInformationService; import com.tyndalehouse.step.core.service.JSwordRelatedVersesService; import com.tyndalehouse.step.core.service.jsword.JSwordMetadataService; import com.tyndalehouse.step.core.service.jsword.JSwordPassageService; import com.tyndalehouse.step.core.service.jsword.JSwordSearchService; import com.tyndalehouse.step.core.service.jsword.JSwordVersificationService; import com.tyndalehouse.step.core.utils.JSwordUtils; import com.tyndalehouse.step.core.utils.StringConversionUtils; import com.tyndalehouse.step.core.utils.StringUtils; import org.apache.lucene.index.Term; import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.TopDocs; import org.apache.lucene.search.TopScoreDocCollector; import org.crosswire.jsword.book.Book; import org.crosswire.jsword.book.BookData; import org.crosswire.jsword.book.BookException; import org.crosswire.jsword.book.OSISUtil; import org.crosswire.jsword.index.lucene.LuceneIndex; import org.crosswire.jsword.passage.Key; import org.crosswire.jsword.passage.KeyUtil; import org.crosswire.jsword.passage.NoSuchKeyException; import org.crosswire.jsword.versification.VersificationsMapper; import org.jdom2.Element; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import java.io.IOException; import java.util.ArrayList; import java.util.List; /** * @author chrisburrell */ public class JSwordRelatedVersesServiceImpl implements JSwordRelatedVersesService { private static final Logger LOG = LoggerFactory.getLogger(JSwordRelatedVersesServiceImpl.class); private static final int SIGNIFICANT_CUT_OFF = 200; private final JSwordSearchService jSwordSearchService; private final JSwordVersificationService jSwordVersificationService; private final JSwordMetadataService jSwordMetadataService; @Inject public JSwordRelatedVersesServiceImpl(final JSwordSearchService jSwordSearchService, final JSwordVersificationService jSwordVersificationService, final JSwordMetadataService jSwordMetadataService) { this.jSwordSearchService = jSwordSearchService; this.jSwordVersificationService = jSwordVersificationService; this.jSwordMetadataService = jSwordMetadataService; } @Override public Key getRelatedVerses(final String version, final String key) { try { //target book, and intermediary strong book final Book targetBook = jSwordVersificationService.getBookFromVersion(version); final Book strongBook = jSwordMetadataService.supportsStrongs(targetBook) ? targetBook : jSwordVersificationService.getBookFromVersion(JSwordPassageService.REFERENCE_BOOK); //target and strong key final Key targetKey = targetBook.getKey(key); final Key strongKey = VersificationsMapper.instance().map(KeyUtil.getPassage(targetKey), jSwordVersificationService.getVersificationForVersion(strongBook)); //get list of strong numbers final String[] strongs = this.getStrongsFromKey(new BookData(strongBook, strongKey)); final IndexSearcher is = jSwordSearchService.getIndexSearcher(strongBook.getInitials()); final List<String> filteredStrongs = keepInfrequentStrongs(strongs, is); return targetBook.getKey(getRelatedVerseReference(filteredStrongs, is)); } catch (final NoSuchKeyException ex) { throw new StepInternalException(ex.getMessage(), ex); } } /** * Keeps the strongs that are less than SIGNIFICANT_CUT_OFF point * * @param strongs the total list of strongs * @param is the index searcher * @return a reduced set of strongs */ private List<String> keepInfrequentStrongs(final String[] strongs, final IndexSearcher is) { final List<String> keepList = new ArrayList<String>(strongs.length); try { for (String s : strongs) { if (is.docFreq(new Term(LuceneIndex.FIELD_STRONG, s)) < SIGNIFICANT_CUT_OFF) { keepList.add(StringConversionUtils.getStrongPaddedKey(s)); } } return keepList; } catch (IOException ex) { throw new StepInternalException(ex.getMessage(), ex); } } /** * Gets the list of all references as a string to be passed to JSword * * @param strongs the list of all strongs * @param is the index searcher * @return the related verses */ private String getRelatedVerseReference(final List<String> strongs, final IndexSearcher is) { try { final BooleanQuery bq = getRelatedLuceneQuery(strongs); final TopScoreDocCollector collector = TopScoreDocCollector.create(50, true); is.search(bq, collector); final TopDocs topDocs = collector.topDocs(); final ScoreDoc[] scoreDocs = topDocs.scoreDocs; final StringBuilder refs = new StringBuilder(128); for (final ScoreDoc scoreDoc : scoreDocs) { final String potentialVerse = is.doc(scoreDoc.doc).get(LuceneIndex.FIELD_KEY); if (refs.length() > 0) { refs.append(' '); } refs.append(potentialVerse); } return refs.toString(); } catch (final IOException ex) { throw new StepInternalException(ex.getMessage(), ex); } } /** * Constructs the query to find all related words * * @param strongs the list of all strongs * @return the query */ private BooleanQuery getRelatedLuceneQuery(final List<String> strongs) { final BooleanQuery bq = new BooleanQuery(); bq.setMinimumNumberShouldMatch(2); for (final String strongNumber : strongs) { // we're going to make a Lucene query to look for at least 2 of the strong numbers bq.add(new TermQuery(new Term(LuceneIndex.FIELD_STRONG, strongNumber)), BooleanClause.Occur.SHOULD); } return bq; } /** * Calculate counts for a particular key. * * @param strongBookData the book data to retrieve strong numbers from */ private String[] getStrongsFromKey(BookData strongBookData) { final StringBuilder strongs = new StringBuilder(256); try { final List<Element> elements = JSwordUtils.getOsisElements(strongBookData); for (final Element e : elements) { if (strongs.length() != 0) { strongs.append(' '); } strongs.append(OSISUtil.getStrongsNumbers(e)); } } catch (final NoSuchKeyException ex) { LOG.warn("Unable to enhance verse numbers.", ex); } catch (final BookException ex) { LOG.warn("Unable to enhance verse number", ex); } return StringUtils.split(strongs.toString()); } }