/******************************************************************************* * Copyright (c) 2012, Directors of the Tyndale STEP Project * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * Neither the name of the Tyndale House, Cambridge (www.TyndaleHouse.com) * nor the names of its contributors may be used to endorse or promote * products derived from this software without specific prior written * permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. ******************************************************************************/ package com.tyndalehouse.step.core.service.search.impl; import com.tyndalehouse.step.core.data.EntityDoc; import com.tyndalehouse.step.core.data.EntityIndexReader; import com.tyndalehouse.step.core.data.EntityManager; import com.tyndalehouse.step.core.exceptions.StepInternalException; import com.tyndalehouse.step.core.exceptions.TranslatedException; import com.tyndalehouse.step.core.models.InterlinearMode; import com.tyndalehouse.step.core.models.LookupOption; import com.tyndalehouse.step.core.models.OsisWrapper; import com.tyndalehouse.step.core.models.search.SubjectEntries; import com.tyndalehouse.step.core.service.jsword.JSwordPassageService; import com.tyndalehouse.step.core.service.jsword.JSwordVersificationService; import com.tyndalehouse.step.core.service.search.SubjectEntrySearchService; import com.tyndalehouse.step.core.utils.StringUtils; import org.apache.lucene.queryParser.QueryParser; import org.crosswire.jsword.book.Book; import org.crosswire.jsword.passage.Key; import org.crosswire.jsword.passage.KeyUtil; import org.crosswire.jsword.passage.NoSuchKeyException; import org.crosswire.jsword.passage.Passage; import org.crosswire.jsword.passage.RangedPassage; import org.crosswire.jsword.passage.RestrictionType; import org.crosswire.jsword.passage.Verse; import org.crosswire.jsword.passage.VerseKey; import org.crosswire.jsword.passage.VerseRange; import org.crosswire.jsword.versification.Versification; import org.crosswire.jsword.versification.VersificationsMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Singleton; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; /** * Retrieves the entries from a subject search * * @author chrisburrell */ @Singleton public class SubjectEntryServiceImpl extends AbstractSubjectSearchServiceImpl implements SubjectEntrySearchService { private static final int AGGREGATING_VERSE_DISTANCE = 10; private static final Logger LOGGER = LoggerFactory.getLogger(SubjectEntryServiceImpl.class); private final EntityIndexReader naves; private final JSwordVersificationService versificationService; private final JSwordPassageService jsword; /** * Instantiates a new subject entry service impl. * * @param entityManager an entity manager providing access to all the different entities. * @param jsword the jsword library * @param versificationService the versification service */ @Inject public SubjectEntryServiceImpl(final EntityManager entityManager, final JSwordPassageService jsword, final JSwordVersificationService versificationService) { super(versificationService); this.jsword = jsword; this.versificationService = versificationService; this.naves = entityManager.getReader("nave"); } @Override public SubjectEntries getSubjectVerses(final String root, final String fullHeader, final String versionList, final String reference, final int context) { final StringBuilder sb = new StringBuilder(root.length() + fullHeader.length() + 64); appendMandatoryField(sb, "root", root); sb.append("+fullHeader:\""); sb.append(QueryParser.escape(fullHeader)); sb.append("\""); final String[] versions = StringUtils.split(versionList, ","); return getVersesForResults(this.naves.search("root", sb.toString()), versions, reference, context); } /** * Appends each part of the query as a mandatory attribute * * @param query the query * @param fieldName the field name * @param value the value */ private void appendMandatoryField(StringBuilder query, String fieldName, String value) { String[] parts = StringUtils.split(value, "[, -=:]+"); for (final String part : parts) { if (StringUtils.isNotBlank(part)) { query.append('+'); query.append(fieldName); query.append(':'); query.append(QueryParser.escape(part)); query.append(' '); } } } /** * obtains the verses for all results * * @param results the results * @param versions the version in which to look it up * @param context the context to expand with the reference * @return the verses */ private SubjectEntries getVersesForResults(final EntityDoc[] results, final String[] versions, final String limitingScopeReference, final int context) { final List<OsisWrapper> verses = new ArrayList<OsisWrapper>(32); boolean masterVersionSwapped = false; for (final EntityDoc doc : results) { final String references = doc.get("references"); masterVersionSwapped |= collectVersesFromReferences(verses, versions, references, limitingScopeReference, context); } return new SubjectEntries(verses, masterVersionSwapped); } /** * Collects individual ranges * * @param verses the verses * @param inputVersions the versions * @param references the list of resultsInKJV that form the results * @param limitingScopeReference the limiting scope for the reference * @param context the context to expand with the reference */ private boolean collectVersesFromReferences(final List<OsisWrapper> verses, final String[] inputVersions, final String references, final String limitingScopeReference, final int context) { final String originalMaster = inputVersions[0]; Passage combinedScopeInKJVv11n = this.getCombinedBookScope(inputVersions); //now let's retain the verses that are of interest in the selected books Key resultsInKJV = null; try { resultsInKJV = this.versificationService.getBookFromVersion(JSwordPassageService.BEST_VERSIFICATION).getKey(references); } catch (NoSuchKeyException e) { throw new StepInternalException("Unable to parse resultsInKJV from Nave", e); } resultsInKJV.retainAll(combinedScopeInKJVv11n); trimResultsToInputSearchRange(inputVersions[0], limitingScopeReference, resultsInKJV); //then calculate what the best version order is GetBestVersionOrderAndKey getBestVersionOrderAndKey = new GetBestVersionOrderAndKey(inputVersions, resultsInKJV).invoke(); Book book = getBestVersionOrderAndKey.getBook(); String[] versions = getBestVersionOrderAndKey.getVersions(); final Passage resultsInProperV11n = getBestVersionOrderAndKey.getVerseRanges(); final Iterator<VerseRange> rangeIterator = resultsInProperV11n.rangeIterator(RestrictionType.NONE); final List<LookupOption> options = new ArrayList<LookupOption>(); options.add(LookupOption.HIDE_XGEN); options.add(LookupOption.GREEK_ACCENTS); options.add(LookupOption.HEBREW_VOWELS); if(context > 0) { //add verse numbers // options.add(LookupOption.TINY_VERSE_NUMBERS); options.add(LookupOption.VERSE_NUMBERS); } final Versification av11n = this.versificationService.getVersificationForVersion(book); Verse lastVerse = null; while (rangeIterator.hasNext()) { final Key range = rangeIterator.next(); // get the distance between the first verse in the range and the last verse if (lastVerse != null && isCloseVerse(av11n, lastVerse, range)) { final OsisWrapper osisWrapper = verses.get(verses.size() - 1); final StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(osisWrapper.getReference()); stringBuilder.append("; "); stringBuilder.append(range.getName()); osisWrapper.setFragment(true); try { osisWrapper.setReference(book.getKey(stringBuilder.toString()).getName()); } catch (final NoSuchKeyException e) { // fail to get a key, let's log and continue LOGGER.warn("Unable to get key for reference: [{}]", osisWrapper.getReference()); LOGGER.trace("Root cause is", e); } } else { final Key firstVerse = this.jsword.getFirstVersesFromRange(range, context); final OsisWrapper passage = this.jsword.peakOsisText(versions, firstVerse, options, InterlinearMode.INTERLEAVED_COMPARE.name()); passage.setReference(range.getName()); if (range.getCardinality() > 1) { passage.setFragment(true); } verses.add(passage); } // record last verse if (range instanceof VerseRange) { final VerseRange verseRange = (VerseRange) range; lastVerse = verseRange.getEnd(); } else if (range instanceof Verse) { lastVerse = (Verse) range; } } return !getBestVersionOrderAndKey.versions[0].equals(originalMaster); } /** * Reduces the results so far to what is contained in the v11n * * @param inputVersion input version * @param limitingScopeReference the limiting scope * @param resultsInKJV the results retrieved so far. */ private void trimResultsToInputSearchRange(final String inputVersion, final String limitingScopeReference, final Key resultsInKJV) { if (StringUtils.isNotBlank(limitingScopeReference)) { final Book limitingBook; limitingBook = this.versificationService.getBookFromVersion(inputVersion); try { final Key key = KeyUtil.getPassage(limitingBook.getKey(limitingScopeReference)); //now map to the KJV versification Passage p = VersificationsMapper.instance().map(KeyUtil.getPassage(key), ((VerseKey) resultsInKJV).getVersification()); //now convert retain against existing resultsInKJV resultsInKJV.retainAll(p); } catch (NoSuchKeyException ex) { throw new TranslatedException(ex, "invalid_reference_in_book", limitingScopeReference, limitingBook.getInitials()); } } } /** * Gets a key in the KJV versification that represents the total combined key for all search resutls. * * @param inputVersions the input version the kjv versified keys * @return */ private Passage getCombinedBookScope(String[] inputVersions) { final Versification bestVersification = this.versificationService.getVersificationForVersion(JSwordPassageService.BEST_VERSIFICATION); Passage range = new RangedPassage(bestVersification); for (final String v : inputVersions) { final Book bookFromVersion = this.versificationService.getBookFromVersion(v); final VerseKey scope = bookFromVersion.getBookMetaData().getScope(); range.addAll(VersificationsMapper.instance().map(KeyUtil.getPassage(scope), bestVersification)); } return range; } /** * @param av11n the versification * @param range the range/verse * @param lastVerse the last verse * @return true if the verse should be wrapped in with the range before */ private boolean isCloseVerse(final Versification av11n, final Verse lastVerse, final Key range) { Verse startOfNextRange = null; if (range instanceof VerseRange) { final VerseRange verseRange = (VerseRange) range; startOfNextRange = verseRange.getStart(); } else if (range instanceof Verse) { startOfNextRange = (Verse) range; } else { // unable to determine whether the verses are close or not... return false; } final int distance = Math.abs(av11n.distance(lastVerse, startOfNextRange)); if (distance < AGGREGATING_VERSE_DISTANCE) { return true; } return false; } /** * A private class that helps rotate the versions around to list the best version available first. */ private class GetBestVersionOrderAndKey { private String[] versions; private Passage resultsInKJV; private Book book; private Passage verseRanges; public GetBestVersionOrderAndKey(String[] versions, Key resultsInKJV) { this.versions = versions; this.resultsInKJV = KeyUtil.getPassage(resultsInKJV); } public Book getBook() { return this.book; } public Passage getVerseRanges() { return this.verseRanges; } public GetBestVersionOrderAndKey invoke() { int maxCardinality = -1; int bestVersion = 0; Book bestBook = null; Key bestKey = this.resultsInKJV; Set<String> triedV11ns = new HashSet<String>(); for (int i = 0; i < versions.length; i++) { String v = versions[i]; Book b = SubjectEntryServiceImpl.this.versificationService.getBookFromVersion(v); final Versification v11n = SubjectEntryServiceImpl.this.versificationService.getVersificationForVersion(b); if (!triedV11ns.contains(v11n)) { final Passage potentialKey = VersificationsMapper.instance() .map(this.resultsInKJV, v11n); int cardinality = potentialKey.getCardinality(); if (cardinality > maxCardinality) { bestVersion = i; maxCardinality = cardinality; bestKey = potentialKey; bestBook = b; } } triedV11ns.add(v11n.getName()); } if (bestVersion != 0) { final String temp = this.versions[bestVersion]; this.versions[bestVersion] = this.versions[0]; this.versions[0] = temp; } this.verseRanges = KeyUtil.getPassage(bestKey); this.book = bestBook; // convert to master version, and be dnoe with it? return this; } public String[] getVersions() { return versions; } } }