Java tutorial
/** * OLAT - Online Learning and Training<br> * http://www.olat.org * <p> * Licensed under the Apache License, Version 2.0 (the "License"); <br> * you may not use this file except in compliance with the License.<br> * You may obtain a copy of the License at * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * Unless required by applicable law or agreed to in writing,<br> * software distributed under the License is distributed on an "AS IS" BASIS, <br> * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> * See the License for the specific language governing permissions and <br> * limitations under the License. * <p> * Copyright (c) frentix GmbH<br> * http://www.frentix.com<br> * <p> */ package org.olat.search.ui; import static org.olat.search.ui.ResultsController.RESULT_PER_PAGE; import java.io.IOException; import java.io.StringReader; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.StringTokenizer; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.analysis.tokenattributes.TermAttribute; import org.apache.lucene.queryParser.ParseException; import org.apache.lucene.util.Version; import org.olat.NewControllerFactory; import org.olat.core.CoreSpringFactory; import org.olat.core.commons.services.search.AbstractOlatDocument; import org.olat.core.commons.services.search.QueryException; import org.olat.core.commons.services.search.ResultDocument; import org.olat.core.commons.services.search.SearchResults; import org.olat.core.commons.services.search.ServiceNotAvailableException; import org.olat.core.commons.services.search.ui.SearchController; import org.olat.core.commons.services.search.ui.SearchEvent; import org.olat.core.commons.services.search.ui.SearchServiceUIFactory; import org.olat.core.commons.services.search.ui.SearchServiceUIFactory.DisplayOption; import org.olat.core.gui.UserRequest; import org.olat.core.gui.components.form.flexible.FormItem; import org.olat.core.gui.components.form.flexible.FormItemContainer; import org.olat.core.gui.components.form.flexible.elements.FormLink; import org.olat.core.gui.components.form.flexible.elements.TextElement; import org.olat.core.gui.components.form.flexible.impl.Form; import org.olat.core.gui.components.form.flexible.impl.FormBasicController; import org.olat.core.gui.components.form.flexible.impl.FormEvent; import org.olat.core.gui.components.link.Link; import org.olat.core.gui.control.Controller; import org.olat.core.gui.control.Event; import org.olat.core.gui.control.WindowControl; import org.olat.core.gui.control.generic.closablewrapper.CloseableModalController; import org.olat.core.gui.control.generic.closablewrapper.CloseableModalWindowController; import org.olat.core.gui.media.RedirectMediaResource; import org.olat.core.id.context.BusinessControl; import org.olat.core.id.context.BusinessControlFactory; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; import org.olat.core.util.StringHelper; import org.olat.search.service.searcher.SearchClientProxy; /** * Description:<br> * Controller with a simple input for the full text search. The display option select how the input is shown: only a button, button with text, input field and button. * <P> * Initial Date: 3 dec. 2009 <br> * * @author srosse, stephane.rosse@frentix.com */ public class SearchInputController extends FormBasicController implements SearchController { private static final OLog log = Tracing.createLoggerFor(SearchInputController.class); private static final String FUZZY_SEARCH = "~0.7"; private static final String CMD_DID_YOU_MEAN_LINK = "didYouMeanLink-"; private static final String SEARCH_STORE_KEY = "search-store-key"; private static final String SEARCH_CACHE_KEY = "search-cache-key"; private String parentContext; private String documentType; private String resourceUrl; private boolean resourceContextEnable = true; private final DisplayOption displayOption; protected FormLink searchButton; protected TextElement searchInput; private ResultsSearchController resultCtlr; private Controller searchDialogBox; protected List<FormLink> didYouMeanLinks; private Map<String, Properties> prefs; private SearchLRUCache searchCache; private SearchClientProxy searchClient; public SearchInputController(final UserRequest ureq, final WindowControl wControl, final String resourceUrl, final DisplayOption displayOption) { super(ureq, wControl, LAYOUT_HORIZONTAL); this.resourceUrl = resourceUrl; this.displayOption = displayOption; setSearchStore(ureq); initForm(ureq); loadPersistedSearch(); loadContext(); } public SearchInputController(final UserRequest ureq, final WindowControl wControl, final String resourceUrl, final String customPage) { super(ureq, wControl, customPage); this.displayOption = DisplayOption.STANDARD_TEXT; this.resourceUrl = resourceUrl; setSearchStore(ureq); initForm(ureq); loadPersistedSearch(); loadContext(); } public SearchInputController(final UserRequest ureq, final WindowControl wControl, final String resourceUrl, final DisplayOption displayOption, final Form mainForm) { super(ureq, wControl, LAYOUT_HORIZONTAL, null, mainForm); this.displayOption = displayOption; this.resourceUrl = resourceUrl; setSearchStore(ureq); initForm(ureq); loadPersistedSearch(); loadContext(); } @Override public String getParentContext() { return parentContext; } @Override public void setParentContext(final String parentContext) { this.parentContext = parentContext; } @Override public String getDocumentType() { return documentType; } @Override public void setDocumentType(final String documentType) { this.documentType = documentType; } @Override public String getResourceUrl() { return resourceUrl; } @Override public void setResourceUrl(final String resourceUrl) { this.resourceUrl = resourceUrl; } @Override public boolean isResourceContextEnable() { return resourceContextEnable; } @Override public void setResourceContextEnable(final boolean resourceContextEnable) { this.resourceContextEnable = resourceContextEnable; } @Override public String getSearchString() { return searchInput.getValue(); } @Override public void setSearchString(final String searchString) { if (StringHelper.containsNonWhitespace(searchString)) { if (searchInput != null) { searchInput.setValue(searchString); } } } @Override protected void initForm(final FormItemContainer formLayout, final Controller listener, final UserRequest ureq) { searchClient = (SearchClientProxy) CoreSpringFactory.getBean("searchClient"); if (displayOption.equals(DisplayOption.STANDARD) || displayOption.equals(DisplayOption.STANDARD_TEXT)) { searchInput = uifactory.addTextElement("search_input", "search.title", 255, "", formLayout); searchInput.setLabel(null, null); } if (displayOption.equals(DisplayOption.STANDARD) || displayOption.equals(DisplayOption.BUTTON)) { searchButton = uifactory.addFormLink("search", "", "", formLayout, Link.NONTRANSLATED + Link.LINK_CUSTOM_CSS); searchButton.setCustomEnabledLinkCSS("o_fulltext_search_button b_small_icon"); } else if (displayOption.equals(DisplayOption.BUTTON_WITH_LABEL)) { searchButton = uifactory.addFormLink("search", formLayout, Link.BUTTON_SMALL); } else if (displayOption.equals(DisplayOption.STANDARD_TEXT)) { final String searchLabel = getTranslator().translate("search"); searchButton = uifactory.addFormLink("search", searchLabel, "", formLayout, Link.NONTRANSLATED + Link.BUTTON_SMALL); } searchButton.setEnabled(true); } private void loadContext() { if (resourceUrl != null) { final ContextTokens context = getContextTokens(resourceUrl); setContext(context); } } protected void setContext(final ContextTokens context) { if (!context.isEmpty()) { final String scope = context.getValueAt(context.getSize() - 1); final String tooltip = getTranslator().translate("form.search.label.tooltip", new String[] { scope }); ((Link) searchButton.getComponent()).setTooltip(tooltip, false); } } private void setSearchStore(final UserRequest ureq) { prefs = (Map<String, Properties>) ureq.getUserSession().getEntry(SEARCH_STORE_KEY); if (prefs == null) { prefs = new HashMap<String, Properties>(); ureq.getUserSession().putEntry(SEARCH_STORE_KEY, prefs); } searchCache = (SearchLRUCache) ureq.getUserSession().getEntry(SEARCH_CACHE_KEY); if (searchCache == null) { searchCache = new SearchLRUCache(); ureq.getUserSession().putEntry(SEARCH_CACHE_KEY, searchCache); } } @Override protected void doDispose() { // } @Override protected void formOK(final UserRequest ureq) { doSearch(ureq); } @Override protected void formNOK(final UserRequest ureq) { doSearch(ureq); } @Override protected void formInnerEvent(final UserRequest ureq, final FormItem source, final FormEvent event) { if (source == searchButton) { doSearch(ureq); } else if (didYouMeanLinks != null && didYouMeanLinks.contains(source)) { final String didYouMeanWord = (String) source.getUserObject(); searchInput.setValue(didYouMeanWord); doSearch(ureq, didYouMeanWord, null, parentContext, documentType, resourceUrl, 0, RESULT_PER_PAGE, false); } } protected void doSearch(final UserRequest ureq) { if (resultCtlr != null) { removeAsListenerAndDispose(resultCtlr); resultCtlr = null; } String oldSearchString = null; final Properties props = getPersistedSearch(); if (props != null) { oldSearchString = props.getProperty("s"); } persistSearch(ureq); if (DisplayOption.BUTTON.equals(displayOption) || DisplayOption.BUTTON_WITH_LABEL.equals(displayOption)) { // no search, only popup createResultsSearchController(ureq); popupResultsSearchController(ureq); if (resultCtlr.getPersistedSearch() != null && !resultCtlr.getPersistedSearch().isEmpty()) { resultCtlr.doSearch(ureq); } } else { final String searchString = getSearchString(); if (StringHelper.containsNonWhitespace(searchString)) { if (oldSearchString != null && !oldSearchString.equals(searchString)) { resetSearch(); } createResultsSearchController(ureq); resultCtlr.setSearchString(searchString); popupResultsSearchController(ureq); resultCtlr.doSearch(ureq); } } } protected Properties getPersistedSearch() { if (getResourceUrl() != null) { final String uri = getResourceUrl(); Properties props = prefs.get(uri); if (props == null) { props = new Properties(); prefs.put(uri, props); } return props; } // not possible but i don't want to trigger a red screen for this if i'm wrong return new Properties(); } protected void resetSearch() { if (getResourceUrl() != null) { final String uri = getResourceUrl(); final Properties props = prefs.get(uri); if (props != null) { prefs.remove(uri); } } } protected void persistSearch(final UserRequest ureq) { if (getResourceUrl() != null) { final String uri = getResourceUrl(); Properties props = prefs.get(uri); if (props == null) { props = new Properties(); } getSearchProperties(props); if (props.isEmpty()) { prefs.remove(uri); } else { prefs.put(uri, props); } } } protected void loadPersistedSearch() { if (getResourceUrl() != null) { final String uri = getResourceUrl(); final Properties props = prefs.get(uri); if (props != null) { setSearchProperties(props); } } } private void createResultsSearchController(final UserRequest ureq) { resultCtlr = new ResultsSearchController(ureq, getWindowControl(), getResourceUrl()); resultCtlr.setDocumentType(getDocumentType()); resultCtlr.setParentContext(getParentContext()); resultCtlr.setResourceContextEnable(isResourceContextEnable()); listenTo(resultCtlr); } protected void getSearchProperties(final Properties props) { if (displayOption.equals(DisplayOption.STANDARD) || displayOption.equals(DisplayOption.STANDARD_TEXT)) { final String searchString = getSearchString(); props.setProperty("s", searchString == null ? "" : searchString); } } protected void setSearchProperties(final Properties props) { if (displayOption.equals(DisplayOption.STANDARD) || displayOption.equals(DisplayOption.STANDARD_TEXT)) { final String searchString = props.getProperty("s"); if (StringHelper.containsNonWhitespace(searchString)) { setSearchString(searchString); } else { setSearchString(""); } } } private void popupResultsSearchController(final UserRequest ureq) { final String title = translate("search.title"); final boolean ajaxOn = getWindowControl().getWindowBackOffice().getWindowManager().isAjaxEnabled(); if (ajaxOn) { searchDialogBox = new CloseableModalWindowController(ureq, getWindowControl(), title, resultCtlr.getInitialComponent(), "ofulltextsearch"); ((CloseableModalWindowController) searchDialogBox).activate(); resultCtlr.listenTo(searchDialogBox); } else { searchDialogBox = new CloseableModalController(getWindowControl(), title, resultCtlr.getInitialComponent()); ((CloseableModalController) searchDialogBox).activate(); } } @Override protected void event(final UserRequest ureq, final Controller source, final Event event) { if (source == resultCtlr) { if (event instanceof SearchEvent) { final SearchEvent goEvent = (SearchEvent) event; final ResultDocument doc = goEvent.getDocument(); gotoSearchResult(ureq, doc); } else if (event == Event.DONE_EVENT) { setSearchString(resultCtlr.getSearchString()); } } else if (CloseableModalWindowController.CLOSE_WINDOW_EVENT.equals(event)) { fireEvent(ureq, Event.DONE_EVENT); } } public void closeSearchDialogBox() { if (searchDialogBox instanceof CloseableModalController) { ((CloseableModalController) searchDialogBox).deactivate(); } else if (searchDialogBox instanceof CloseableModalWindowController) { ((CloseableModalWindowController) searchDialogBox).deactivate(); } searchDialogBox = null; } /** * @param ureq * @param command */ @Override public void gotoSearchResult(final UserRequest ureq, final ResultDocument document) { try { // attach the launcher data closeSearchDialogBox(); final String url = document.getResourceUrl(); if (!StringHelper.containsNonWhitespace(url)) { // no url, no document getWindowControl().setWarning(getTranslator().translate("error.resource.could.not.found")); } else if (url != null && url.startsWith("[ContextHelpModule:")) { // do something special for ContextHelp final int pathIndex = url.indexOf("path="); final String uri = url.substring(pathIndex + 5, url.length() - 1); final RedirectMediaResource rsrc = new RedirectMediaResource(uri); ureq.getDispatchResult().setResultingMediaResource(rsrc); } else { final BusinessControl bc = BusinessControlFactory.getInstance().createFromString(url); final WindowControl bwControl = BusinessControlFactory.getInstance().createBusinessWindowControl(bc, getWindowControl()); NewControllerFactory.getInstance().launch(ureq, bwControl); } } catch (final Exception ex) { log.debug("Document not found"); getWindowControl().setWarning(getTranslator().translate("error.resource.could.not.found")); } } protected SearchResults doSearch(final UserRequest ureq, final String searchString, final List<String> condSearchStrings, final String parentCtxt, final String docType, final String rsrcUrl, final int firstResult, final int maxReturns, final boolean doSpellCheck) { String query = null; List<String> condQueries = null; try { if (doSpellCheck) { // remove first old "did you mean words" hideDidYouMeanWords(); } getHighlightWords(searchString); query = getQueryString(searchString, false); condQueries = getCondQueryStrings(condSearchStrings, parentCtxt, docType, rsrcUrl); SearchResults searchResults = searchCache.get(getQueryCacheKey(firstResult, query, condQueries)); if (searchResults == null || true) { searchResults = searchClient.doSearch(query, condQueries, ureq.getIdentity(), ureq.getUserSession().getRoles(), firstResult, maxReturns, true); searchCache.put(getQueryCacheKey(firstResult, query, condQueries), searchResults); } if ((firstResult == 0 && searchResults.getList().isEmpty()) && !query.endsWith(FUZZY_SEARCH)) { // result-list was empty => first try to find word via spell-checker if (doSpellCheck) { final Set<String> didYouMeansWords = searchClient.spellCheck(searchString); if (didYouMeansWords != null && !didYouMeansWords.isEmpty()) { setDidYouMeanWords(didYouMeansWords); } else { searchResults = doFuzzySearch(ureq, searchString, null, parentCtxt, docType, rsrcUrl, firstResult, maxReturns); } } else { searchResults = doFuzzySearch(ureq, searchString, null, parentCtxt, docType, rsrcUrl, firstResult, maxReturns); } } if (firstResult == 0 && searchResults.getList().isEmpty()) { showInfo("found.no.result.try.fuzzy.search"); } return searchResults; } catch (final ParseException e) { if (log.isDebug()) { log.debug("Query cannot be parsed: " + query); } getWindowControl().setWarning(translate("invalid.search.query")); } catch (final QueryException e) { getWindowControl().setWarning(translate("invalid.search.query.with.wildcard")); } catch (final ServiceNotAvailableException e) { getWindowControl().setWarning(translate("search.service.not.available")); } catch (final Exception e) { log.error("Unexpected exception while searching", e); getWindowControl().setWarning(translate("search.service.unexpected.error")); } return SearchResults.EMPTY_SEARCH_RESULTS; } protected Set<String> getHighlightWords(final String searchString) { try { final Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_CURRENT); final TokenStream stream = analyzer.tokenStream("content", new StringReader(searchString)); final TermAttribute termAtt = stream.addAttribute(TermAttribute.class); for (boolean next = stream.incrementToken(); next; next = stream.incrementToken()) { final String term = termAtt.term(); if (log.isDebug()) { log.debug(term); } } } catch (final IOException e) { log.error("", e); } return null; } protected SearchResults doFuzzySearch(final UserRequest ureq, final String searchString, final List<String> condSearchStrings, final String parentCtxt, final String docType, final String rsrcUrl, final int firstResult, final int maxReturns) throws QueryException, ParseException, ServiceNotAvailableException { hideDidYouMeanWords(); final String query = getQueryString(searchString, true); final List<String> condQueries = getCondQueryStrings(condSearchStrings, parentCtxt, docType, rsrcUrl); SearchResults searchResults = searchCache.get(getQueryCacheKey(firstResult, query, condQueries)); if (searchResults == null) { searchResults = searchClient.doSearch(query, condQueries, ureq.getIdentity(), ureq.getUserSession().getRoles(), firstResult, maxReturns, true); searchCache.put(getQueryCacheKey(firstResult, query, condQueries), searchResults); } return searchResults; } private Object getQueryCacheKey(final int firstResult, final String query, final List<String> condQueries) { final StringBuilder sb = new StringBuilder(); sb.append('[').append(firstResult).append(']').append(query).append(' '); for (final String condQuery : condQueries) { sb.append(condQuery).append(' '); } return sb.toString(); } public Set<String> getDidYouMeanWords() { if (didYouMeanLinks != null && !didYouMeanLinks.isEmpty()) { final Set<String> didYouMeanWords = new HashSet<String>(); for (final FormLink link : didYouMeanLinks) { final String word = (String) link.getUserObject(); didYouMeanWords.add(word); } return didYouMeanWords; } return Collections.emptySet(); } /** * Unregister existing did-you-mean-links from content and add new links. * * @param didYouMeansWords List of 'did you mean' words */ public void setDidYouMeanWords(final Set<String> didYouMeansWords) { // unregister existing did-you-mean links hideDidYouMeanWords(); didYouMeanLinks = new ArrayList<FormLink>(didYouMeansWords.size()); int wordNumber = 0; for (final String word : didYouMeansWords) { final FormLink l = uifactory.addFormLink(CMD_DID_YOU_MEAN_LINK + wordNumber++, word, null, flc, Link.NONTRANSLATED); l.setUserObject(word); didYouMeanLinks.add(l); } flc.contextPut("didYouMeanLinks", didYouMeanLinks); flc.contextPut("hasDidYouMean", Boolean.TRUE); } protected void hideDidYouMeanWords() { // unregister existing did-you-mean links if (didYouMeanLinks != null) { for (int i = 0; i < didYouMeanLinks.size(); i++) { flc.remove(CMD_DID_YOU_MEAN_LINK + i); } didYouMeanLinks = null; } flc.contextPut("didYouMeanLinks", didYouMeanLinks); flc.contextPut("hasDidYouMean", Boolean.FALSE); } private String getQueryString(final String searchString, final boolean fuzzy) { final StringBuilder query = new StringBuilder(searchString); if (fuzzy) { query.append(FUZZY_SEARCH); } return query.toString(); } private List<String> getCondQueryStrings(final List<String> condSearchStrings, final String parentCtxt, final String docType, final String rsrcUrl) { final List<String> queries = new ArrayList<String>(); if (condSearchStrings != null && !condSearchStrings.isEmpty()) { queries.addAll(condSearchStrings); } if (StringHelper.containsNonWhitespace(parentCtxt)) { appendAnd(queries, AbstractOlatDocument.PARENT_CONTEXT_TYPE_FIELD_NAME, ":\"", parentCtxt, "\""); } if (StringHelper.containsNonWhitespace(docType)) { appendAnd(queries, "(", AbstractOlatDocument.DOCUMENTTYPE_FIELD_NAME, ":(", docType, "))"); } if (StringHelper.containsNonWhitespace(rsrcUrl)) { appendAnd(queries, AbstractOlatDocument.RESOURCEURL_FIELD_NAME, ":", escapeResourceUrl(rsrcUrl), "*"); } return queries; } private void appendAnd(final List<String> queries, final String... strings) { final StringBuilder query = new StringBuilder(); for (final String string : strings) { query.append(string); } if (query.length() > 0) { queries.add(query.toString()); } } /** * Remove the ROOT keyword, duplicate entry in the business path and escape the keywords used by lucene. * * @param url * @return */ protected String escapeResourceUrl(final String url) { final List<String> tokens = getResourceUrlTokenized(url); final StringBuilder sb = new StringBuilder(); for (final String token : tokens) { sb.append("\\[").append(token.replace(":", "\\:")).append("\\]"); } return sb.toString(); } protected List<String> getResourceUrlTokenized(String url) { if (url.startsWith("ROOT")) { url = url.substring(4, url.length()); } final List<String> tokens = new ArrayList<String>(); for (final StringTokenizer tokenizer = new StringTokenizer(url, "[]"); tokenizer.hasMoreTokens();) { final String token = tokenizer.nextToken(); if (!tokens.contains(token)) { tokens.add(token); } } return tokens; } protected ContextTokens getContextTokens(final String resourceURL) { final SearchServiceUIFactory searchUIFactory = (SearchServiceUIFactory) CoreSpringFactory .getBean(SearchServiceUIFactory.class); final List<String> tokens = getResourceUrlTokenized(resourceURL); final String[] keys = new String[tokens.size() + 1]; final String[] values = new String[tokens.size() + 1]; keys[0] = ""; values[0] = translate("search.context.all"); final StringBuilder sb = new StringBuilder(); for (int i = 0; i < tokens.size(); i++) { final String token = tokens.get(i); keys[i + 1] = sb.append('[').append(token).append(']').toString(); values[i + 1] = searchUIFactory.getBusinessPathLabel(token, tokens, getLocale()); } return new ContextTokens(keys, values); } @Override public FormItem getFormItem() { return flc; } public class ContextTokens { private final String[] keys; private final String[] values; public ContextTokens(final String[] keys, final String[] values) { this.keys = keys == null ? new String[0] : keys; this.values = values == null ? new String[0] : values; } public String[] getKeys() { return keys; } public String[] getValues() { return values; } public boolean isEmpty() { return values.length == 0; } public int getSize() { return values.length; } public String getKeyAt(final int index) { if (keys != null && index < keys.length && index >= 0) { return keys[index]; } return ""; } public String getValueAt(final int index) { if (values != null && index < values.length && index >= 0) { return values[index]; } return ""; } } }