All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gwt.user.client.ui.MultiWordSuggestOracle Maven / Gradle / Ivy

There is a newer version: 2.10.0
Show newest version
/*
 * Copyright 2007 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.gwt.user.client.ui;

import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.safehtml.shared.annotations.IsSafeHtml;
import com.google.gwt.user.client.rpc.IsSerializable;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.TreeSet;

/**
 * The default {@link com.google.gwt.user.client.ui.SuggestOracle}. The default
 * oracle returns potential suggestions based on breaking the query into
 * separate words and looking for matches. It also modifies the returned text to
 * show which prefix matched the query term. The matching is case insensitive.
 * All suggestions are sorted before being passed into a response.
 * 

* Example Table *

*

*

* * * * * * * * * * * * * * * * * * * * *
All Suggestions Query stringMatching Suggestions
John Smith, Joe Brown, Jane Doe, Jane Smith, Bob JonesJoJohn Smith, Joe Brown, Bob Jones
John Smith, Joe Brown, Jane Doe, Jane Smith, Bob JonesSmithJohn Smith, Jane Smith
Georgia, New York, CaliforniagGeorgia
*

*/ public class MultiWordSuggestOracle extends SuggestOracle { /** * Suggestion class for {@link MultiWordSuggestOracle}. */ public static class MultiWordSuggestion implements Suggestion, IsSerializable { @IsSafeHtml private String displayString; private String replacementString; /** * Constructor used by RPC. */ public MultiWordSuggestion() { } /** * Constructor for MultiWordSuggestion. * * @param replacementString the string to enter into the SuggestBox's text * box if the suggestion is chosen * @param displayString the display string */ public MultiWordSuggestion(String replacementString, @IsSafeHtml String displayString) { this.replacementString = replacementString; this.displayString = displayString; } @IsSafeHtml public String getDisplayString() { return displayString; } public String getReplacementString() { return replacementString; } } /** * A class reresenting the bounds of a word within a string. * * The bounds are represented by a {@code startIndex} (inclusive) and * an {@code endIndex} (exclusive). */ private static class WordBounds implements Comparable { final int startIndex; final int endIndex; public WordBounds(int startIndex, int length) { this.startIndex = startIndex; this.endIndex = startIndex + length; } public int compareTo(WordBounds that) { int comparison = this.startIndex - that.startIndex; if (comparison == 0) { comparison = that.endIndex - this.endIndex; } return comparison; } } private static final char WHITESPACE_CHAR = ' '; private static final String WHITESPACE_STRING = " "; /** * Regular expression used to collapse all whitespace in a query string. */ private static final String NORMALIZE_TO_SINGLE_WHITE_SPACE = "\\s+"; /** * Associates substrings with words. */ private final PrefixTree tree = new PrefixTree(); /** * Associates individual words with candidates. */ private HashMap> toCandidates = new HashMap>(); /** * Associates candidates with their formatted suggestions. Multiple formatted * suggestions could be normalized to the same candidate, e.g. both 'Mobile' * and 'MOBILE' are normalized to 'mobile'. */ private HashMap> toRealSuggestions = new HashMap<>(); /** * Specifies whether all formatted suggestions should be returned per * normalized candidate. Refer ro {@link #setSuggestAllMatchingWords} * for more details. */ private boolean suggestAllMatchingWords = false; /** * The whitespace masks used to prevent matching and replacing of the given * substrings. */ private char[] whitespaceChars; private Response defaultResponse; /* * Comparator used for sorting candidates from search. */ private Comparator comparator = null; /** * Constructor for MultiWordSuggestOracle. This uses a space as * the whitespace character. * * @see #MultiWordSuggestOracle(String) */ public MultiWordSuggestOracle() { this(" "); } /** * Constructor for MultiWordSuggestOracle which takes in a set of * whitespace chars that filter its input. *

* Example: If ".," is passed in as whitespace, then the string * "foo.bar" would match the queries "foo", "bar", "foo.bar", "foo...bar", and * "foo, bar". If the empty string is used, then all characters are used in * matching. For example, the query "bar" would match "bar", but not "foo * bar". *

* * @param whitespaceChars the characters to treat as word separators */ public MultiWordSuggestOracle(String whitespaceChars) { this.whitespaceChars = new char[whitespaceChars.length()]; for (int i = 0; i < whitespaceChars.length(); i++) { this.whitespaceChars[i] = whitespaceChars.charAt(i); } } /** * Adds a suggestion to the oracle. Each suggestion must be plain text. * * @param suggestion the suggestion */ public void add(String suggestion) { String candidate = normalizeSuggestion(suggestion); // candidates --> real suggestions. List realSuggestions = toRealSuggestions.get(candidate); if (realSuggestions == null) { realSuggestions = new ArrayList(); toRealSuggestions.put(candidate, realSuggestions); } realSuggestions.add(0, suggestion); // word fragments --> candidates. String[] words = candidate.split(WHITESPACE_STRING); for (int i = 0; i < words.length; i++) { String word = words[i]; tree.add(word); Set l = toCandidates.get(word); if (l == null) { l = new HashSet(); toCandidates.put(word, l); } l.add(candidate); } } /** * Adds all suggestions specified. Each suggestion must be plain text. * * @param collection the collection */ public final void addAll(Collection collection) { for (String suggestion : collection) { add(suggestion); } } /** * Removes all of the suggestions from the oracle. */ public void clear() { tree.clear(); toCandidates.clear(); toRealSuggestions.clear(); } @Override public boolean isDisplayStringHTML() { return true; } @Override public void requestDefaultSuggestions(Request request, Callback callback) { if (defaultResponse != null) { callback.onSuggestionsReady(request, defaultResponse); } else { super.requestDefaultSuggestions(request, callback); } } @Override public void requestSuggestions(Request request, Callback callback) { String query = normalizeSearch(request.getQuery()); int limit = request.getLimit(); // Get candidates from search words. List candidates = createCandidatesFromSearch(query); // Respect limit for number of choices. int numberTruncated = Math.max(0, candidates.size() - limit); for (int i = candidates.size() - 1; i > limit; i--) { candidates.remove(i); } // Convert candidates to suggestions. List suggestions = convertToFormattedSuggestions(query, candidates); Response response = new Response(suggestions); response.setMoreSuggestionsCount(numberTruncated); callback.onSuggestionsReady(request, response); } /** * Sets the comparator used for sorting candidates from search. * * @param comparator the comparator to use. */ public void setComparator(Comparator comparator) { this.comparator = comparator; } /** * Sets the default suggestion collection. * * @param suggestionList the default list of suggestions */ public void setDefaultSuggestions(Collection suggestionList) { this.defaultResponse = new Response(suggestionList); } /** * A convenience method to set default suggestions using plain text strings. * * Note to use this method each default suggestion must be plain text. * * @param suggestionList the default list of suggestions */ public final void setDefaultSuggestionsFromText( Collection suggestionList) { Collection accum = new ArrayList(); for (String candidate : suggestionList) { accum.add(createSuggestion(candidate, SafeHtmlUtils.htmlEscape(candidate))); } setDefaultSuggestions(accum); } /** * Sets the flag on whether to suggest all matching words. With words * 'Mobile', 'MOBILE', 'mobile', 'MoBILE', typing 'm' will only build one * suggestion for 'MoBILE' if {@code suggestAllMatchingWords} is * {@code false}. However, it will build suggestions for all four words if * {@code suggestAllMatchingWords} is {@code true}. * * @param suggestAllMatchingWords true to return all formatted suggestions * per normalized candidate, false to return the last formatted * suggestions per normalized candidate. */ public final void setSuggestAllMatchingWords(boolean suggestAllMatchingWords) { this.suggestAllMatchingWords = suggestAllMatchingWords; } /** * Creates the suggestion based on the given replacement and display strings. * * @param replacementString the string to enter into the SuggestBox's text box * if the suggestion is chosen * @param displayString the display string * * @return the suggestion created */ protected MultiWordSuggestion createSuggestion( String replacementString, @IsSafeHtml String displayString) { return new MultiWordSuggestion(replacementString, displayString); } /** * Returns real suggestions with the given query in strong html * font. * * @param query query string * @param candidates candidates * @return real suggestions */ private List convertToFormattedSuggestions(String query, List candidates) { List suggestions = new ArrayList(); for (int i = 0; i < candidates.size(); i++) { String candidate = candidates.get(i); // Use real suggestion for assembly. List realSuggestions = toRealSuggestions.get(candidate); TreeSet realSuggestionsSet = new TreeSet<>(); if (suggestAllMatchingWords) { realSuggestionsSet.addAll(realSuggestions); } else { // Build only one suggestion per normalized candidate if // suggestAllMatchingWords is false. realSuggestionsSet.add(realSuggestions.get(0)); } Iterator realSuggestionsIterator = realSuggestionsSet.iterator(); while (realSuggestionsIterator.hasNext()) { int cursor = 0; int index = 0; String formattedSuggestion = realSuggestionsIterator.next(); // Create strong search string. SafeHtmlBuilder accum = new SafeHtmlBuilder(); String[] searchWords = query.split(WHITESPACE_STRING); while (true) { WordBounds wordBounds = findNextWord(candidate, searchWords, index); if (wordBounds == null) { break; } if (wordBounds.startIndex == 0 || WHITESPACE_CHAR == candidate.charAt(wordBounds.startIndex - 1)) { String part1 = formattedSuggestion.substring(cursor, wordBounds.startIndex); String part2 = formattedSuggestion.substring(wordBounds.startIndex, wordBounds.endIndex); cursor = wordBounds.endIndex; accum.appendEscaped(part1); accum.appendHtmlConstant(""); accum.appendEscaped(part2); accum.appendHtmlConstant(""); } index = wordBounds.endIndex; } // Check to make sure the search was found in the string. if (cursor == 0) { continue; } accum.appendEscaped(formattedSuggestion.substring(cursor)); MultiWordSuggestion suggestion = createSuggestion(formattedSuggestion, accum.toSafeHtml().asString()); suggestions.add(suggestion); } } return suggestions; } /** * Find the sorted list of candidates that are matches for the given query. */ private List createCandidatesFromSearch(String query) { ArrayList candidates = new ArrayList(); if (query.length() == 0) { return candidates; } // Find all words to search for. String[] searchWords = query.split(WHITESPACE_STRING); HashSet candidateSet = null; for (int i = 0; i < searchWords.length; i++) { String word = searchWords[i]; // Eliminate bogus word choices. if (word.length() == 0 || word.matches(WHITESPACE_STRING)) { continue; } // Find the set of candidates that are associated with all the // searchWords. HashSet thisWordChoices = createCandidatesFromWord(word); if (candidateSet == null) { candidateSet = thisWordChoices; } else { candidateSet.retainAll(thisWordChoices); if (candidateSet.size() < 2) { // If there is only one candidate, on average it is cheaper to // check if that candidate contains our search string than to // continue intersecting suggestion sets. break; } } } if (candidateSet != null) { candidates.addAll(candidateSet); Collections.sort(candidates, comparator); } return candidates; } /** * Creates a set of potential candidates that match the given query. * * @param query query string * @return possible candidates */ private HashSet createCandidatesFromWord(String query) { HashSet candidateSet = new HashSet(); List words = tree.getSuggestions(query, Integer.MAX_VALUE); if (words != null) { // Find all candidates that contain the given word the search is a // subset of. for (int i = 0; i < words.size(); i++) { Collection belongsTo = toCandidates.get(words.get(i)); if (belongsTo != null) { candidateSet.addAll(belongsTo); } } } return candidateSet; } /** * Returns a {@link WordBounds} representing the first word in {@code * searchWords} that is found in candidate starting at {@code indexToStartAt} * or {@code null} if no words could be found. */ private WordBounds findNextWord(String candidate, String[] searchWords, int indexToStartAt) { WordBounds firstWord = null; for (String word : searchWords) { int index = candidate.indexOf(word, indexToStartAt); if (index != -1) { WordBounds newWord = new WordBounds(index, word.length()); if (firstWord == null || newWord.compareTo(firstWord) < 0) { firstWord = newWord; } } } return firstWord; } /** * Normalize the search key by making it lower case, removing multiple spaces, * apply whitespace masks, and make it lower case. */ private String normalizeSearch(String search) { // Use the same whitespace masks and case normalization for the search // string as was used with the candidate values. search = normalizeSuggestion(search); // Remove all excess whitespace from the search string. search = search.replaceAll(NORMALIZE_TO_SINGLE_WHITE_SPACE, WHITESPACE_STRING); return search.trim(); } /** * Takes the formatted suggestion, makes it lower case and blanks out any * existing whitespace for searching. */ private String normalizeSuggestion(String formattedSuggestion) { // Formatted suggestions should already have normalized whitespace. So we // can skip that step. // Lower case suggestion. formattedSuggestion = formattedSuggestion.toLowerCase(Locale.ROOT); // Apply whitespace. if (whitespaceChars != null) { for (int i = 0; i < whitespaceChars.length; i++) { char ignore = whitespaceChars[i]; formattedSuggestion = formattedSuggestion.replace(ignore, WHITESPACE_CHAR); } } return formattedSuggestion; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy