package com.tyndalehouse.step.core.service.impl; import com.tyndalehouse.step.core.models.AvailableFeatures; import com.tyndalehouse.step.core.models.ClientSession; import com.tyndalehouse.step.core.models.InterlinearMode; import com.tyndalehouse.step.core.models.LookupOption; import com.tyndalehouse.step.core.models.TrimmedLookupOption; import com.tyndalehouse.step.core.service.BibleInformationService; import com.tyndalehouse.step.core.service.PassageOptionsValidationService; import com.tyndalehouse.step.core.service.jsword.JSwordMetadataService; import javax.inject.Inject; import javax.inject.Provider; import javax.inject.Singleton; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.ResourceBundle; import java.util.Set; import static com.tyndalehouse.step.core.models.InterlinearMode.INTERLINEAR; import static com.tyndalehouse.step.core.models.InterlinearMode.NONE; import static com.tyndalehouse.step.core.models.LookupOption.ENGLISH_VOCAB; import static com.tyndalehouse.step.core.models.LookupOption.GREEK_VOCAB; import static com.tyndalehouse.step.core.models.LookupOption.HEADINGS; import static com.tyndalehouse.step.core.models.LookupOption.MORPHOLOGY; import static com.tyndalehouse.step.core.models.LookupOption.NOTES; import static com.tyndalehouse.step.core.models.LookupOption.TRANSLITERATION; import static com.tyndalehouse.step.core.models.LookupOption.VERSE_NUMBERS; import static com.tyndalehouse.step.core.utils.StringUtils.isBlank; /** * @author chrisburrell */ @Singleton public class PassageOptionsValidationServiceImpl implements PassageOptionsValidationService { private final Provider<ClientSession> clientSessionProvider; private final JSwordMetadataService jswordMetadata; @Inject public PassageOptionsValidationServiceImpl(final JSwordMetadataService jswordMetadata, final Provider<ClientSession> clientSessionProvider) { this.jswordMetadata = jswordMetadata; this.clientSessionProvider = clientSessionProvider; } @Override public List<LookupOption> getLookupOptions(final String options) { final List<LookupOption> lookupOptions = new ArrayList<>(); if (isBlank(options)) { return lookupOptions; } for (int ii = 0; ii < options.length(); ii++) { lookupOptions.add(LookupOption.fromUiOption(options.charAt(ii))); } return lookupOptions; } @Override public Set<LookupOption> trim(final List<LookupOption> options, final String version, List<String> extraVersions, final InterlinearMode mode, final List<TrimmedLookupOption> trimmingExplanations) { // if we're not explaining why features aren't available, we don't overwrite the display mode final InterlinearMode displayMode = determineDisplayMode(options, mode, trimmingExplanations != null); return trim(options, version, extraVersions, mode, displayMode, trimmingExplanations); } @Override public Set<LookupOption> trim(final List<LookupOption> options, final String version, List<String> extraVersions, final InterlinearMode mode, final InterlinearMode displayMode, final List<TrimmedLookupOption> trimmingExplanations) { // obtain error messages final ResourceBundle errors = ResourceBundle.getBundle("ErrorBundle", this.clientSessionProvider .get().getLocale()); if (options.isEmpty()) { return new HashSet<>(); } final Set<LookupOption> result = getUserOptionsForVersion(errors, options, version, extraVersions, trimmingExplanations); // now trim further depending on modes required: switch (displayMode) { case COLUMN: case COLUMN_COMPARE: case INTERLEAVED: case INTERLEAVED_COMPARE: removeInterleavingOptions(errors, trimmingExplanations, result, !mode.equals(displayMode)); break; case INTERLINEAR: explainRemove(errors, NOTES, result, trimmingExplanations, !mode.equals(displayMode), errors.getString("option_not_available_interlinear")); result.add(LookupOption.VERSE_NEW_LINE); break; case NONE: break; default: break; } return result; } @Override public InterlinearMode determineDisplayMode(final List<LookupOption> options, final InterlinearMode mode, final boolean realMode) { if (realMode && mode == NONE && hasInterlinearOption(options)) { return INTERLINEAR; } return mode; } /** * Removes the interleaving options. * * @param errors the error mesages * @param trimmingExplanations explanations on why something was removed * @param result result * @param originalModeHasChanged true to indicate that the chosen display mode has been forced upon the user */ private void removeInterleavingOptions(final ResourceBundle errors, final List<TrimmedLookupOption> trimmingExplanations, final Set<LookupOption> result, final boolean originalModeHasChanged) { final String interleavedMessage = errors.getString("option_not_available_interleaved"); explainRemove(errors, VERSE_NUMBERS, result, trimmingExplanations, originalModeHasChanged, interleavedMessage); explainRemove(errors, NOTES, result, trimmingExplanations, originalModeHasChanged, interleavedMessage); explainRemove(errors, ENGLISH_VOCAB, result, trimmingExplanations, originalModeHasChanged, interleavedMessage); explainRemove(errors, GREEK_VOCAB, result, trimmingExplanations, originalModeHasChanged, interleavedMessage); explainRemove(errors, TRANSLITERATION, result, trimmingExplanations, originalModeHasChanged, interleavedMessage); explainRemove(errors, MORPHOLOGY, result, trimmingExplanations, originalModeHasChanged, interleavedMessage); //if we're going for a headings only, then we need to leave headings on if (!result.contains(LookupOption.HEADINGS_ONLY)) { explainRemove(errors, HEADINGS, result, trimmingExplanations, originalModeHasChanged, interleavedMessage); } } /** * explains why an option has been removed. * * @param errors the errors * @param option the option we want to remove * @param result the resulting options * @param trimmingOptions the list of options * @param originalModeChanged tru if the original mode has changed * @param explanation the explanation */ private void explainRemove(final ResourceBundle errors, final LookupOption option, final Set<LookupOption> result, final List<TrimmedLookupOption> trimmingOptions, final boolean originalModeChanged, final String explanation) { if (result.remove(option) && trimmingOptions != null) { final TrimmedLookupOption trimmedOption; if (originalModeChanged) { final StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(explanation); stringBuilder.append(" "); stringBuilder.append(errors.getString("option_not_available_other")); trimmedOption = new TrimmedLookupOption(stringBuilder.toString(), option); } else { trimmedOption = new TrimmedLookupOption(explanation, option); } trimmingOptions.add(trimmedOption); } } /** * @param options the options that have been selected * @return true if one of the options requires an interlinear */ private boolean hasInterlinearOption(final List<LookupOption> options) { return options.contains(LookupOption.GREEK_VOCAB) || options.contains(LookupOption.MORPHOLOGY) || options.contains(LookupOption.ENGLISH_VOCAB) || options.contains(LookupOption.TRANSLITERATION); } /** * Given a set of options selected by the user and a verson, retrieves the options that are actually available * * @param errors the error messages * @param options the options given by the user * @param version the version of interest * @param extraVersions the secondary versions that affect feature resolution * @param trimmingExplanations the explanations of why options are being removed * @return a potentially smaller set of options that are actually possible */ private Set<LookupOption> getUserOptionsForVersion(final ResourceBundle errors, final List<LookupOption> options, final String version, final List<String> extraVersions, final List<TrimmedLookupOption> trimmingExplanations) { final Set<LookupOption> available = this.jswordMetadata.getFeatures(version, extraVersions); final Set<LookupOption> result = new HashSet<>(options.size()); // do a crazy bubble intersect, but it's tiny so that's fine for (final LookupOption loOption : options) { boolean added = false; for (final LookupOption avOption : available) { if (loOption.equals(avOption)) { result.add(loOption); added = true; break; } } // option not available in that particular version if (trimmingExplanations != null && !added) { trimmingExplanations.add(new TrimmedLookupOption(errors .getString("option_not_supported_by_version"), loOption)); } } return result; } @Override public AvailableFeatures getAvailableFeaturesForVersion(final String version, final List<String> extraVersions, final String inputDisplayMode, final InterlinearMode finalDisplayMode) { final List<LookupOption> allLookupOptions = Arrays.asList(LookupOption.values()); final Set<LookupOption> outcome = trim(allLookupOptions, version, extraVersions, getDisplayMode(inputDisplayMode, version, extraVersions), finalDisplayMode, null); return new AvailableFeatures(new ArrayList<>(outcome), null); } @Override public InterlinearMode getDisplayMode(final String interlinearMode, final String mainBook, final List<String> extraVersions) { InterlinearMode userDesiredMode = isBlank(interlinearMode) ? NONE : InterlinearMode.valueOf(interlinearMode); return this.jswordMetadata.getBestInterlinearMode(mainBook, extraVersions, userDesiredMode); } @Override public String optionsToString(final Collection<LookupOption> options) { StringBuilder codedOptions = new StringBuilder(); for (LookupOption o : options) { if (o.getUiName() != BibleInformationService.UNAVAILABLE_TO_UI) { codedOptions.append(o.getUiName()); } } return codedOptions.toString(); } }