/******************************************************************************* * 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.impl; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; import javax.inject.Inject; import javax.inject.Named; import com.tyndalehouse.step.core.exceptions.StepInternalException; import com.tyndalehouse.step.core.models.KeyWrapper; import com.tyndalehouse.step.core.models.LexiconSuggestion; import com.tyndalehouse.step.core.models.stats.ScopeType; import com.tyndalehouse.step.core.models.stats.StatType; import com.tyndalehouse.step.core.models.search.ExpandableSubjectHeadingEntry; import com.tyndalehouse.step.core.models.search.SearchEntry; import com.tyndalehouse.step.core.models.search.SearchResult; import com.tyndalehouse.step.core.models.stats.CombinedPassageStats; import com.tyndalehouse.step.core.models.stats.PassageStat; import com.tyndalehouse.step.core.service.AnalysisService; import com.tyndalehouse.step.core.service.LexiconDefinitionService; import com.tyndalehouse.step.core.service.jsword.JSwordAnalysisService; import com.tyndalehouse.step.core.service.jsword.JSwordPassageService; import com.tyndalehouse.step.core.service.jsword.impl.JSwordAnalysisServiceImpl; import com.tyndalehouse.step.core.service.search.SubjectSearchService; import com.tyndalehouse.step.core.utils.StringUtils; import org.crosswire.jsword.passage.Key; import org.crosswire.jsword.passage.Verse; /** * A service able to retrieve various kinds of statistics, delegates to {@link JSwordAnalysisServiceImpl} for * some operations. * * @author chrisburrell */ public class AnalysisServiceImpl implements AnalysisService { public static final String OSIS_CHAPTER_STARTS_WITH = ".* "; public static final Pattern CLEAN_UP_DIGITS = Pattern.compile("[0-9]+\\.?\\w?"); private final Set<String> stopSubjects; private int maxWords; private final SubjectSearchService subjects; private final LexiconDefinitionService definitions; private JSwordPassageService jSwordPassageService; private final JSwordAnalysisService jswordAnalysis; /** * Creates a service able to retrieve various stats. * * @param jswordAnalysis the jsword analysis * @param subjects the subjects * @param definitions the definitions */ @Inject public AnalysisServiceImpl(final JSwordAnalysisServiceImpl jswordAnalysis, @Named("analysis.maxWords") int maxWords, @Named("analysis.stopSubjects") String stopSubjects, final SubjectSearchService subjects, final LexiconDefinitionService definitions, JSwordPassageService jSwordPassageService) { this.jswordAnalysis = jswordAnalysis; this.maxWords = maxWords; this.subjects = subjects; this.definitions = definitions; this.jSwordPassageService = jSwordPassageService; this.stopSubjects = StringUtils.createSet(stopSubjects); } @Override public CombinedPassageStats getStatsForPassage( final String version, final String reference, final StatType statType, final ScopeType scopeType, boolean nextChapter) { final String keyResolutionVersion = statType == StatType.TEXT ? version : JSwordPassageService.REFERENCE_BOOK; final KeyWrapper centralReference = nextChapter ? jSwordPassageService.getSiblingChapter(reference, keyResolutionVersion , false): jSwordPassageService.getKeyInfo(reference, keyResolutionVersion, keyResolutionVersion); final CombinedPassageStats statsForPassage = new CombinedPassageStats(); PassageStat stat; switch (statType) { case WORD: stat = this.jswordAnalysis.getWordStats(centralReference.getKey(), scopeType); stat.trim(maxWords); statsForPassage.setLexiconWords(convertWordStatsToDefinitions(stat)); break; case TEXT: stat = this.jswordAnalysis.getTextStats(version, centralReference.getKey(), scopeType); stat.trim(maxWords); break; case SUBJECT: stat = getSubjectStats(version, centralReference.getName(), scopeType); stat.trim(maxWords); break; default: throw new StepInternalException("Unsupported type of stat asked for."); } stat.setReference(centralReference); statsForPassage.setPassageStat(stat); return statsForPassage; } /** * Converts the stats from numbers to their equivalent definition * * @param passageStat the retrieved strongs * @return the set of lexical entries associated with these keys */ private Map<String, LexiconSuggestion> convertWordStatsToDefinitions(final PassageStat passageStat) { final Map<String, LexiconSuggestion> lexiconEntries = this.definitions.lookup(passageStat.getStats().keySet()); return lexiconEntries; } /** * Subject stats. * * @param version the version * @param reference the reference * @param scopeType * @return the passage stat */ private PassageStat getSubjectStats(final String version, final String reference, final ScopeType scopeType) { final SearchResult subjectResults = this.subjects.searchByReference(getReferenceSyntax(reference, version, scopeType)); final PassageStat stat = new PassageStat(); //we duplicate the set here because we'd like to keep the casing... final List<SearchEntry> results = subjectResults.getResults(); for (final SearchEntry entry : results) { if (entry instanceof ExpandableSubjectHeadingEntry) { final ExpandableSubjectHeadingEntry subjectEntry = (ExpandableSubjectHeadingEntry) entry; //we will first do the subheading because ideally we want that 'case' to be the master case, //i.e. David rather than DAVID final String subjectHeading = subjectEntry.getHeading(); if (subjectHeading != null && !stopSubjects.contains(subjectHeading.toUpperCase())) { stat.addWordTryCases(CLEAN_UP_DIGITS.matcher(subjectHeading).replaceAll("")); } final String root = subjectEntry.getRoot(); if (root != null && !stopSubjects.contains(root.toUpperCase())) { stat.addWordTryCases(CLEAN_UP_DIGITS.matcher(root).replaceAll(root)); } } } return stat; } /** * Creates a lucene query to allow search for multiple chapters/entire books, without generating * thousands of boolean queries, because we're expanding a book into all its verses! * * @param version the version in which look up the key * @param scopeType the scope type */ private String getReferenceSyntax(final String reference, final String version, final ScopeType scopeType) { final KeyWrapper key = this.jSwordPassageService.getKeyInfo(reference, version, version); final Key total = key.getKey(); StringBuilder sb = new StringBuilder(32); switch (scopeType) { case PASSAGE: case CHAPTER: case NEAR_BY_CHAPTER: //expand all the chapters.... int minChapter = -1; int maxChapter = -1; Verse firstVerse = null; Verse lastVerse; //need to expand between chapters.... final Iterator<Key> iterator = total.iterator(); Verse v = null; while (iterator.hasNext()) { final Key next = iterator.next(); if (next instanceof Verse) { v = (Verse) next; if (minChapter == -1) { minChapter = v.getChapter(); firstVerse = v; } int currentChapter = v.getChapter(); if (currentChapter != maxChapter) { sb.append(v.getBook().getOSIS()); sb.append('.'); sb.append(v.getChapter()); sb.append(".* "); } maxChapter = v.getChapter(); } } lastVerse = v; //need to add +1 and -1 if (scopeType == ScopeType.NEAR_BY_CHAPTER) { sb.append(firstVerse.getBook().getOSIS()); sb.append('.'); sb.append(minChapter - 1); sb.append(".* "); sb.append(lastVerse.getBook().getOSIS()); sb.append('.'); sb.append(minChapter - 1); sb.append(".* "); } break; case BOOK: Key k = key.getKey().get(0); if (k instanceof Verse) { sb.append(((Verse) k).getBook().getOSIS()); sb.append(OSIS_CHAPTER_STARTS_WITH); } break; default: throw new StepInternalException("Unsupported option."); } return sb.toString(); } }