package com.tyndalehouse.step.rest.controllers; import com.tyndalehouse.step.core.models.AbstractComplexSearch; import com.tyndalehouse.step.core.models.ClientSession; import com.tyndalehouse.step.core.models.InterlinearMode; import com.tyndalehouse.step.core.models.LexiconSuggestion; import com.tyndalehouse.step.core.models.OsisWrapper; import com.tyndalehouse.step.core.models.SearchToken; import com.tyndalehouse.step.core.models.search.SearchResult; import com.tyndalehouse.step.core.service.AppManagerService; import com.tyndalehouse.step.core.service.LanguageService; import com.tyndalehouse.step.core.utils.StringUtils; import com.tyndalehouse.step.core.utils.language.ContemporaryLanguageUtils; import com.yammer.metrics.annotation.Timed; import org.codehaus.jackson.map.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Provider; import javax.inject.Singleton; import javax.servlet.ServletException; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.awt.*; import java.io.IOException; import java.util.List; import java.util.Locale; import java.util.MissingResourceException; import java.util.ResourceBundle; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; /** * @author chrisburrell */ @Singleton public class SearchPageController extends HttpServlet { private static final Pattern COMMA_SEPARATORS = Pattern.compile(","); public static String DEV_TOKEN = "UA-36285759-2"; public static String LIVE_TOKEN = "UA-36285759-1"; private static Logger LOGGER = LoggerFactory.getLogger(SearchPageController.class); private final SearchController search; private final ModuleController modules; private final BibleController bible; private final LanguageService languageService; private final AppManagerService appManagerService; private final Provider<ObjectMapper> objectMapper; private final Provider<ClientSession> clientSessionProvider; @Inject public SearchPageController(final SearchController search, final ModuleController modules, final BibleController bible, final LanguageService languageService, final AppManagerService appManagerService, Provider<ObjectMapper> objectMapper, Provider<ClientSession> clientSessionProvider) { this.search = search; this.modules = modules; this.bible = bible; this.languageService = languageService; this.appManagerService = appManagerService; this.objectMapper = objectMapper; this.clientSessionProvider = clientSessionProvider; } @Override @Timed(name = "home-page", group = "pages", rateUnit = TimeUnit.SECONDS, durationUnit = TimeUnit.MILLISECONDS) protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException { if(!checkLanguage()) { //do redirect //clear the lang cookie final Cookie[] cookies = request.getCookies(); if(cookies != null) { for(Cookie c : cookies) { if("lang".equals(c.getName())) { c.setMaxAge(0); response.addCookie(c); } } doRedirect(response); return; } return; } AbstractComplexSearch text; try { //if we have a 'reference' and/or 'version' parameter, redirect to that page final String oldVersion = request.getParameter("version"); final String oldReference = request.getParameter("reference"); if (StringUtils.isNotBlank(oldVersion) || StringUtils.isNotBlank(oldReference)) { doRedirect(response, oldReference, oldVersion); return; } text = doSearch(request); setupRequestContext(request, text); setupResponseContext(response); } catch (Exception exc) { LOGGER.warn(exc.getMessage(), exc); } finally { request.getRequestDispatcher("/start.jsp").include(request, response); } } private boolean checkLanguage() { Locale userLocale = this.clientSessionProvider.get().getLocale(); if(userLocale.getLanguage() == null) { return true; } return this.languageService.isSupported(userLocale.getLanguage(), userLocale.getCountry()); } private void doRedirect(final HttpServletResponse response, final String oldReference, final String oldVersion) { try { response.setStatus(301); response.setHeader("Location", String.format("http://%s/?q=%s", appManagerService.getAppDomain(), getUrlFragmentForPassage(oldVersion, oldReference))); response.setHeader("Connection", "close"); } catch (Exception ex) { LOGGER.error("Failed to operate redirect", ex); return; } } private void doRedirect(final HttpServletResponse response) { try { response.setStatus(301); response.setHeader("Location", String.format("http://%s", appManagerService.getAppDomain())); response.setHeader("Connection", "close"); } catch (Exception ex) { LOGGER.error("Failed to operate redirect", ex); return; } } /** * Sets up default attributes on response * * @param resp the response */ private void setupResponseContext(final HttpServletResponse resp) { resp.setContentType("text/html"); resp.setCharacterEncoding("UTF-8"); } /** * Sets up the request context for use in the JSTL parsing * * @param req the request * @param data the osisWrapper * @throws IOException */ private void setupRequestContext(final HttpServletRequest req, final AbstractComplexSearch data) throws IOException { //global settings //set the language attributes once final Locale userLocale = this.clientSessionProvider.get().getLocale(); req.setAttribute("languageCode", userLocale.getLanguage()); req.setAttribute("languageName", ContemporaryLanguageUtils.capitaliseFirstLetter(userLocale .getDisplayLanguage(userLocale)).replace("\"", "")); req.setAttribute("languageComplete", this.languageService.isCompleted(userLocale.getLanguage())); req.setAttribute("ltr", ComponentOrientation.getOrientation(userLocale).isLeftToRight()); req.setAttribute("versions", objectMapper.get().writeValueAsString(modules.getAllModules())); req.setAttribute("searchType", data.getSearchType().name()); req.setAttribute("versionList", getVersionList(data.getMasterVersion(), data.getExtraVersions())); req.setAttribute("languages", this.languageService.getAvailableLanguages()); //specific to passages if (data instanceof OsisWrapper) { final OsisWrapper osisWrapper = (OsisWrapper) data; req.setAttribute("passageText", osisWrapper.getValue()); osisWrapper.setValue(null); req.setAttribute("passageModel", objectMapper.get().writeValueAsString(osisWrapper)); populateMetaPassage(req, osisWrapper); populateSiblingChapters(req, osisWrapper); } else if (data instanceof SearchResult) { final SearchResult results = (SearchResult) data; req.setAttribute("searchResults", results.getResults()); req.setAttribute("definitions", results.getDefinitions()); req.setAttribute("filter", results.getStrongHighlights()); req.setAttribute("numResults", results.getTotal()); req.setAttribute("sort", results.getOrder()); results.setResults(null); req.setAttribute("passageModel", objectMapper.get().writeValueAsString(results)); populateMetaSearch(req, results); } //set the analytics token req.setAttribute("stepDomain", appManagerService.getAppDomain()); req.setAttribute("analyticsToken", Boolean.TRUE.equals(Boolean.getBoolean("step.development")) ? DEV_TOKEN : LIVE_TOKEN); } /** * Returns a list of versions used in the search * * * @param masterVersion the first version * @param extraVersions the extra versions * @return the properly formatted list */ private String[] getVersionList(final String masterVersion, final String extraVersions) { if (StringUtils.isBlank(masterVersion)) { return new String[0]; } if (StringUtils.isBlank(extraVersions)) { return new String[] { masterVersion} ; } final String[] extras = StringUtils.split(extraVersions, ","); final String[] allVersions = new String[extras.length + 1]; allVersions[0] = masterVersion; int ii = 1; for(String e : extras) { allVersions[ii++] = e; } return allVersions; } /** * Creates the fragments for previous and next chapters * * @param req the request * @param osisWrapper the osiswrapper */ private void populateSiblingChapters(final HttpServletRequest req, final OsisWrapper osisWrapper) { req.setAttribute("previousChapter", getUrlFragmentForPassage(osisWrapper.getMasterVersion(), osisWrapper.getPreviousChapter().getOsisKeyId())); req.setAttribute("nextChapter", getUrlFragmentForPassage(osisWrapper.getMasterVersion(), osisWrapper.getNextChapter().getOsisKeyId())); } /** * Populates the meta data for a search * * @param req the request * @param results the results */ private void populateMetaSearch(final HttpServletRequest req, final SearchResult results) { try { final List<SearchToken> searchTokens = results.getSearchTokens(); String scope = null; StringBuilder keyInfo = new StringBuilder(128); StringBuilder versions = new StringBuilder(32); for (SearchToken t : searchTokens) { final String tokenType = t.getTokenType(); final String token = t.getToken(); final String infoSeparator = " | "; if (SearchToken.VERSION.equals(tokenType)) { versions.append(token); versions.append(infoSeparator); } else if (SearchToken.REFERENCE.equals(tokenType)) { scope = token; } else if (SearchToken.SUBJECT_SEARCH.equals(tokenType)) { keyInfo.append(token); versions.append(infoSeparator); } else if (SearchToken.NAVE_SEARCH.equals(tokenType)) { keyInfo.append(token); versions.append(infoSeparator); } else if (SearchToken.NAVE_SEARCH_EXTENDED.equals(tokenType)) { keyInfo.append(token); versions.append(infoSeparator); } else if (SearchToken.TEXT_SEARCH.equals(tokenType)) { keyInfo.append(token); versions.append(infoSeparator); } else if (SearchToken.STRONG_NUMBER.equals(tokenType)) { final LexiconSuggestion enhancedTokenInfo = (LexiconSuggestion) t.getEnhancedTokenInfo(); keyInfo.append(enhancedTokenInfo.getMatchingForm()); keyInfo.append(" - "); keyInfo.append(enhancedTokenInfo.getGloss()); keyInfo.append(" - "); keyInfo.append(enhancedTokenInfo.getStepTransliteration()); keyInfo.append(" - "); keyInfo.append(enhancedTokenInfo.getStrongNumber()); keyInfo.append(infoSeparator); } else if (SearchToken.MEANINGS.equals(tokenType)) { keyInfo.append(token); keyInfo.append(infoSeparator); } else if (SearchToken.TOPIC_BY_REF.equals(tokenType)) { keyInfo.append(token); keyInfo.append(infoSeparator); } else if (SearchToken.RELATED_VERSES.equals(tokenType)) { keyInfo.append(token); keyInfo.append(infoSeparator); } else if (SearchToken.EXACT_FORM.equals(tokenType)) { keyInfo.append(token); keyInfo.append(infoSeparator); } else if (SearchToken.SYNTAX.equals(tokenType)) { keyInfo.append(token); keyInfo.append(infoSeparator); } } if (scope != null) { keyInfo.append(scope); keyInfo.append(" | "); } final String trimmedInfo = keyInfo.toString().trim(); final String relevantInfo = (keyInfo.length() > 2 ? trimmedInfo.substring(0, trimmedInfo.length() - 2) : trimmedInfo).replaceAll("\\|", ","); req.setAttribute("description", ResourceBundle.getBundle("ErrorBundle").getString("search_results_for") + " " + relevantInfo); try { keyInfo.append(ResourceBundle.getBundle("ErrorBundle", clientSessionProvider.get().getLocale()).getString(results.getSearchType().getLanguageSearchKey())); } catch (MissingResourceException ex) { //swallow LOGGER.warn("Missing resource for {}", results.getSearchType().getLanguageSearchKey(), ex); keyInfo.append("Search"); } req.setAttribute("title", wrapTitle(keyInfo.toString(), results.getMasterVersion(), null)); req.setAttribute("canonicalUrl", req.getParameter("q")); } catch (Exception ex) { //a page with no title is better than no pages LOGGER.error("Unable to ascertain meta data", ex); } } /** * Returns the title of a passage * * @param osisWrapper the text already retrieved * @return the title */ private void populateMetaPassage(final HttpServletRequest request, final OsisWrapper osisWrapper) { try { final String preview = this.bible.getPlainTextPreview(osisWrapper.getMasterVersion(), osisWrapper.getOsisId()); request.setAttribute("title", wrapTitle(osisWrapper.getReference(), osisWrapper.getMasterVersion(), preview)); request.setAttribute("description", preview); request.setAttribute("canonicalUrl", getUrlFragmentForPassage(osisWrapper.getMasterVersion(), osisWrapper.getOsisId())); } catch (Exception ex) { //a page with no title is better than no pages LOGGER.error("Unable to ascertain meta data", ex); } } /** * Obtains a URL fragment for a given passage * * @param version the master version * @param reference the reference * @return the url */ private String getUrlFragmentForPassage(final String version, final String reference) { final StringBuilder fragment = new StringBuilder(128); if (StringUtils.isNotBlank(version)) { fragment.append(SearchToken.VERSION).append("="); fragment.append(version); fragment.append("|"); } if (StringUtils.isNotBlank(reference)) { fragment.append(SearchToken.REFERENCE).append("=").append(reference); } return fragment.toString(); } /** * @param keyInfo key bit of information that should be at the forefront of the URL * @param masterVersion * @param preview * @return */ private String wrapTitle(final String keyInfo, final String masterVersion, final String preview) { StringBuilder sb = new StringBuilder(); sb.append(keyInfo); sb.append(" | "); sb.append(masterVersion); sb.append(" | "); sb.append("STEP"); if (preview != null) { sb.append(" | "); sb.append(preview); } return sb.toString(); } private AbstractComplexSearch doSearch(final HttpServletRequest req) { AbstractComplexSearch text; try { text = this.search.masterSearch( req.getParameter("q"), req.getParameter("options"), req.getParameter("display"), req.getParameter("page"), req.getParameter("qFilter"), req.getParameter("sort"), req.getParameter("context")); } catch (Exception ex) { LOGGER.warn(ex.getMessage(), ex); text = getDefaultPassage(); } return text; } /** * Defaults to Matt.1 if can't do anything else * * @return Matt 1 or something else */ private AbstractComplexSearch getDefaultPassage() { AbstractComplexSearch text; try { text = this.search.masterSearch("reference=Gen.1|version=ESV", "HNV"); } catch (Exception e) { LOGGER.error("Default search failed", e); text = new OsisWrapper("", null, new String[]{"en"}, null, "ESV-THE", InterlinearMode.NONE, ""); } return text; } }