{"id":419,"date":"2010-07-14T12:32:14","date_gmt":"2010-07-14T20:32:14","guid":{"rendered":"http:\/\/www.zackgrossbart.com\/hackito\/?p=419"},"modified":"2010-07-30T05:00:39","modified_gmt":"2010-07-30T13:00:39","slug":"gwt-rest-auto","status":"publish","type":"post","link":"http:\/\/www.zackgrossbart.com\/hackito\/gwt-rest-auto\/","title":{"rendered":"Creating a Multi-Valued Auto-Complete Field Using GWT SuggestBox and REST"},"content":{"rendered":"<link type=\"text\/css\" rel=\"stylesheet\" href=\"\/extras\/multisuggest\/MultivalueSuggestBoxExample.css\"> <script type=\"text\/javascript\" language=\"javascript\" src=\"\/extras\/multisuggest\/multivaluesuggestboxexample\/multivaluesuggestboxexample.nocache.js\"><\/script> <iframe src=\"javascript:\" id=\"__gwt_historyFrame\" tabIndex=\"-1\" style=\"position:absolute;width:0;height:0;border:0\"><\/iframe><br \/>\n<style type=\"text\/css\"> body {font-size: 10px;} <\/style>\n<p>\nThis is a guest post written by <a href=\"#bess\">Bess Siegal<\/a>.  Bess and I work together and she recently created an auto-complete field that handles multiple values.  She stopped by to show us how it works, provide an open source example, and give you all the details you need to create a multi-value auto-complete field in <a href=\"http:\/\/code.google.com\/webtoolkit\/\">GWT<\/a> using <a href=\"\/hackito\/gwt-rest\/\">REST<\/a>.\n<\/p>\n<h2>More Than Just Another Type-Ahead Script<\/h2>\n<p>\nType ahead fields are ubiquitous.  Type <i>ja<\/i> into Google and it prompts you for Jamn 94.5, Java, and Jack Johnson.  These single value fields work well for search, but don&#8217;t scale to multiple values.  Our latest tool allows users to search for multiple users, roles, and other objects using an auto-complete field, and for that we needed something new.\n<\/p>\n<p>\nThis article shows you how to create a multi-valued auto-complete field using the GWT SuggestBox and REST.  Communicating between the client and server with REST gives this example a strong separation between the client and the server.  I proved it using a  server-side component written as a Java servlet, then swapping it out, thanks to <a href=\"\/hackito\/about-2\/\">Zack<\/a>, with a server-side component written in PHP.\n<\/p>\n<p>\nThis sample uses JavaScript, HTML, CSS, and nothing else.  It runs in all the major browsers and all the way back to Internet Explorer 6.  It&#8217;s also a good introduction to REST and how to use it from a browser.\n<\/p>\n<h2>Try It<\/h2>\n<p>The multi-value auto-complete field can handle any type of data.  Here&#8217;s an example that searches for crayon colors.  <\/p>\n<h4>Search for blue, mac, or *<\/h4>\n<div id=\"multivalueFieldContainer\"><\/div>\n<div id=\"sendButtonContainer\"><\/div>\n<p><div id=\"valuesContainer\"><\/div>\n<div id=\"valueMapContainer\"><\/div>\n<\/p>\n<h2>A Closer Look<\/h2>\n<p>I began by investigating the GWT <a href=\"http:\/\/gwt.google.com\/samples\/KitchenSink\/KitchenSink.html#Lists\">SuggestBox<\/a>.  It&#8217;s not multi-valued, but it does automatically send a request and shows a suggestion list popup when you type into its text field. By default its suggestions come from a <a href=\"http:\/\/google-web-toolkit.googlecode.com\/svn\/javadoc\/2.0\/com\/google\/gwt\/user\/client\/ui\/SuggestBox.html\"><code>MultiWordSuggestOracle<\/code><\/a>, which means all the suggestions reside on the client.\n<\/p>\n<p><a href=\"http:\/\/development.lombardi.com\/?p=39\">Using the GWT SuggestBox with RPC<\/a> by <a href=\"http:\/\/development.lombardi.com\/?author=3\">Alex Moffat<\/a> explains how to retrieve suggestions from a remote source using RPC by creating a custom <a href=\"http:\/\/google-web-toolkit.googlecode.com\/svn\/javadoc\/2.0\/com\/google\/gwt\/user\/client\/ui\/SuggestOracle.html\"><code>SuggestOracle<\/code><\/a>.  It got me started on creating my own <code>SuggestOracle<\/code> using REST instead of using GWT&#8217;s RPC.  The <a href=\"http:\/\/jquery.bassistance.de\/autocomplete\/demo\/\">jQuery auto-complete plugin<\/a> by <a href=\"http:\/\/bassistance.de\/about\/\">J\u00f6rn Zaefferer<\/a> inspired me to make it multi-valued.  <\/p>\n<h3>Working Example<\/h3>\n<div class=\"sourcebox\">\nGet the<br \/>\n<a href=\"\/extras\/multisuggest\/MultivalueSuggestBoxExample.zip\">source code<\/a>\n<\/div>\n<p>I&#8217;ll walk you through the working code example and explain how it:<\/p>\n<ul>\n<li>retrieves suggestions using REST<\/li>\n<li>allows for multiple selection<\/li>\n<li>performs as an auto-completer even for multiple values that have been pasted in<\/li>\n<li>allows for browsing through large data sets without having to use a div overflow scrollbar<\/li>\n<\/ul>\n<h4>REST Endpoint<\/h4>\n<p>\nThe REST Endpoint can be any HTTP URL that supporst GET and takes the following parameters.<\/p>\n<ul>\n<li>q &#8211; the query<\/li>\n<li>indexFrom &#8211; the 0-based starting index<\/li>\n<li>indexTo &#8211; the last index, inclusive<\/li>\n<\/ul>\n<p>The endpoint returns <a href=\"http:\/\/www.json.org\/\">JSON<\/a> in the following format:<\/p>\n<pre>\r\n{ \r\n \"TotalSize\" : 25,\r\n \"Options\" : [\r\n              {\"Value\" : \"#9ACEEB\", \"DisplayName\" : \"Cornflower\"},\r\n              {\"Value\" : \"#CC6666\", \"DisplayName\" : \"Fuzzy Wuzzy\"}\r\n             ]\r\n}\r\n<\/pre>\n<p>&#8220;TotalSize&#8221; is the total number of results yielded by the query and &#8220;Options&#8221; is the array of name-value pairs in the results from indexFrom and to indexTo.<\/p>\n<p>This well-defined API allows for a server implementation in any language.  The <a href=\"http:\/\/www.zackgrossbart.com\/extras\/multisuggest\/MultivalueSuggestBoxExample.html\">source code<\/a> includes a Java servlet that serves this purpose and also the endpoint Zack wrote in <a href=\"http:\/\/php.net\/index.php\">PHP<\/a> that drives the <a href=\"http:\/\/www.zackgrossbart.com\/extras\/multisuggest\/MultivalueSuggestBoxExample.html\">live demo<\/a> in this article.\n<\/p>\n<h4>MultivalueSuggestBox<\/h4>\n<p>\nThe <code>MultivalueSuggestBox<\/code> is a custom GWT widget that utilizes the GWT <a href=\"http:\/\/google-web-toolkit.googlecode.com\/svn\/javadoc\/2.0\/com\/google\/gwt\/user\/client\/ui\/SuggestBox.html\"><code>SuggestBox<\/code><\/a>.  It extends <a href=\"http:\/\/google-web-toolkit.googlecode.com\/svn\/javadoc\/2.0\/com\/google\/gwt\/user\/client\/ui\/Composite.html\"><code>Composite<\/code><\/a> with a <a href=\"http:\/\/google-web-toolkit.googlecode.com\/svn\/javadoc\/2.0\/com\/google\/gwt\/user\/client\/ui\/FlowPanel.html\"><code>FlowPanel<\/code><\/a> as its main widget.  It encapsulates six inner classes that support server-side data retrieval.\n<\/p>\n<p>\nBelow is the constructor.  Its arguments are the REST endpoint URL and a boolean to specify multi-value.<span class=\"footnote\"><a name=\"suggnote_note1\" href=\"#suggnote_code1\">1<\/a><\/span>  A <a href=\"http:\/\/google-web-toolkit.googlecode.com\/svn\/javadoc\/2.0\/com\/google\/gwt\/user\/client\/ui\/FlowPanel.html\"><code>FlowPanel<\/code><\/a><span class=\"footnote\"><a name=\"suggnote_note2\" href=\"#suggnote_code2\">2<\/a><\/span> is instantiated so that a <code>FormFeedback<\/code><span class=\"footnote\"><a name=\"suggnote_note3\" href=\"#suggnote_code3\">3<\/a><\/span> control can be included next to the <code>SuggestBox<\/code>.  The <code>FormFeedback<\/code> is a custom GWT widget that shows an icon indicating the status of the control.  This lets the user know whether the control is loading and more importantly, helps to identify invalid values.\n<\/p>\n<p>\nA <code>SuggestBox<\/code> is instantiated using the <code>RestSuggestOracle<\/code><span class=\"footnote\"><a name=\"suggnote_note4\" href=\"#suggnote_code4\">4<\/a><\/span>.<br \/>\nThe <code>m_valueMap<\/code><span class=\"footnote\"><a name=\"suggnote_note5\" href=\"#suggnote_code5\">5<\/a><\/span> is instantiated to hold all the valid values found, keyed by name. <\/p>\n<pre>    \r\n\/**\r\n * Constructor.\r\n * @param the URL for the REST endpoint.  \r\n * @param isMultivalued - true for allowing multiple values\r\n *\/\r\npublic MultivalueSuggestBox(String restEndpointUrl, boolean isMultivalued)<span class=\"footnote\"><a name=\"suggnote_code1\" href=\"#suggnote_note1\">1<\/a><\/span>\r\n{\r\n    m_restEndpointUrl = restEndpointUrl;\r\n    m_isMultivalued = isMultivalued;\r\n\r\n    FlowPanel panel = new FlowPanel();<span class=\"footnote\"><a href=\"#suggnote_note2\" name=\"suggnote_code2\">2<\/a><\/span>\r\n    TextBoxBase textfield;\r\n    if (isMultivalued) {\r\n        panel.addStyleName(\"textarearow\");\r\n        textfield = new TextArea();\r\n    } else {\r\n        panel.addStyleName(\"textfieldrow\");\r\n        textfield = new TextBox();\r\n    }\r\n        \r\n    \/\/Create our own SuggestOracle that queries REST endpoint\r\n    SuggestOracle oracle = new RestSuggestOracle();<span class=\"footnote\"><a href=\"#suggnote_note4\" name=\"suggnote_code4\">4<\/a><\/span>\r\n    \/\/intialize the SuggestBox\r\n    m_field = new SuggestBox(oracle, textfield);\r\n    if (isMultivalued) {\r\n        \/\/have to do this here b\/c gwt suggest box wipes \r\n        \/\/style name if added in previous if\r\n        textfield.addStyleName(\"multivalue\");            \r\n    }\r\n    m_field.addStyleName(\"wideTextField\");\r\n    m_field.addSelectionHandler(this);\r\n        \r\n    panel.add(m_field);\r\n    m_feedback = new FormFeedback();<span class=\"footnote\"><a href=\"#suggnote_note3\" name=\"suggnote_code3\">3<\/a><\/span>\r\n    panel.add(m_feedback);        \r\n        \r\n    initWidget(panel);\r\n      \r\n    m_valueMap = new HashMap&lt;String, String&gt;();<span class=\"footnote\"><a href=\"#suggnote_note5\" name=\"suggnote_code5\">5<\/a><\/span>\r\n       \r\n    resetPageIndices();        \r\n}\r\n<\/pre>\n<\/p>\n<h4>RestSuggestOracle<\/h4>\n<p>\n<code>RestSuggestOracle<\/code><span class=\"footnote\"><a name=\"suggnote_note6\" href=\"#suggnote_code6\">6<\/a><\/span>, like all the other classes mentioned in the remainder of this article, is an inner class within <code>MultivalueSuggestBox<\/code>.  It extends <a href=\"http:\/\/google-web-toolkit.googlecode.com\/svn\/javadoc\/2.0\/com\/google\/gwt\/user\/client\/ui\/SuggestOracle.html\"><code>SuggestOracle<\/code><\/a> implementing <code>requestSuggestions<\/code><span class=\"footnote\"><a name=\"suggnote_note7\" href=\"#suggnote_code7\">7<\/a><\/span> and overriding <code>isDisplayStringHTML<\/code> to return true.  It creates a timer<span class=\"footnote\"><a name=\"suggnote_note8\" href=\"#suggnote_code8\">8<\/a><\/span> that is reset whenever the user types<span class=\"footnote\"><a name=\"suggnote_note9\" href=\"#suggnote_code9\">9<\/a><\/span> because we only want to query the server when the user has paused in their typing.  This will prevent overloading the server with unnecessary overlapping requests.  After the timer has elapsed, <code>getSuggestions<\/code><span class=\"footnote\"><a name=\"suggnote_note10\" href=\"#suggnote_code10\">10<\/a><\/span> and conditionally <code>findExactMatches<\/code><span class=\"footnote\"><a name=\"suggnote_note11\" href=\"#suggnote_code11\">11<\/a><\/span> are called. <\/p>\n<pre>\r\n\/**\r\n* A custom Suggest Oracle\r\n*\/\r\nprivate class RestSuggestOracle<span class=\"footnote\"><a name=\"suggnote_code6\" href=\"#suggnote_note6\">6<\/a><\/span> extends SuggestOracle\r\n{\r\nprivate SuggestOracle.Request m_request;\r\nprivate SuggestOracle.Callback m_callback;\r\nprivate Timer m_timer;\r\n\r\nRestSuggestOracle()\r\n{\r\n    m_timer = new Timer()<span class=\"footnote\"><a name=\"suggnote_code8\" href=\"#suggnote_note8\">8<\/a><\/span> {\r\n        \r\n        @Override\r\n        public void run()\r\n        {\r\n            \/*\r\n             * The reason we check for empty string is found at\r\n             * http:\/\/development.lombardi.com\/?p=39 --\r\n             * paraphrased, if you backspace quickly the contents of the field \r\n             * are emptied but a query for a single character is still executed.\r\n             * Workaround for this is to check for an empty string field here.\r\n             *\/\r\n            \r\n            if (!m_field.getText().trim().isEmpty()) {\r\n                if (m_isMultivalued) {\r\n                    \/\/calling this here in case a user is trying to correct the \r\n                    \/\/\"kev\" value of Allison Andrews, Kev, Josh Nolan or pasted \r\n                    \/\/in multiple values\r\n                    findExactMatches();<span class=\"footnote\"><a name=\"suggnote_code11\" href=\"#suggnote_note11\">11<\/a><\/span>\r\n                }                    \r\n                getSuggestions();<span class=\"footnote\"><a name=\"suggnote_code10\" href=\"#suggnote_note10\">10<\/a><\/span>\r\n            }\r\n        }\r\n    };\r\n}\r\n\r\n@Override\r\npublic void requestSuggestions(SuggestOracle.Request request, \r\n\t\t\t       SuggestOracle.Callback callback)<span class=\"footnote\"><a name=\"suggnote_code7\" href=\"#suggnote_note7\">7<\/a><\/span>\r\n{                \r\n    \/\/This is the method that gets called by the SuggestBox whenever typing in the text field            \r\n    m_request = request;\r\n    m_callback = callback;\r\n    \r\n    \/\/reset the indexes (b\/c NEXT and PREV call getSuggestions directly)\r\n    resetPageIndices();\r\n    \r\n    \/\/If the user keeps triggering this event (e.g., keeps typing), cancel and restart the timer\r\n    m_timer.cancel();        \r\n    m_timer.schedule(DELAY);<span class=\"footnote\"><a name=\"suggnote_code9\" href=\"#suggnote_note9\">9<\/a><\/span>\r\n}\r\n\r\n<\/pre>\n<\/p>\n<h4>getSuggestions<\/h4>\n<p>First we&#8217;ll look at <code>getSuggestions<\/code><span class=\"footnote\"><a name=\"suggnote_note12\" href=\"#suggnote_code12\">12<\/a><\/span>.  This method performs the query for the contents of the suggestion list popup.  First, it determines the search term.  When not multi-valued, the search term is the contents of the suggest box text field.  If it is  multi-valued, the search term is the part of the string after the last <code>DISPLAY_SEPARATOR<\/code>, in this case a comma. It then sets the <code>FormFeedback<\/code> widget to a <code>LOADING<\/code> state, then calls <code>queryOptions<\/code>.   <\/p>\n<pre>\r\nprivate void getSuggestions()<span class=\"footnote\"><a name=\"suggnote_code12\" href=\"#suggnote_note12\">12<\/a><\/span>\r\n{\r\n    String query = m_request.getQuery();\r\n    \r\n    \/\/find the last thing entered up to the last separator\r\n    \/\/and use that as the query\r\n    if (m_isMultivalued) {\r\n        int sep = query.lastIndexOf(DISPLAY_SEPARATOR);\r\n        if (sep &gt; 0) {\r\n            query = query.substring(sep + DISPLAY_SEPARATOR.length());                \r\n        }\r\n    }\r\n    query = query.trim();\r\n    \r\n    \/\/do not query if it's just an empty String\r\n    \/\/also do not get suggestions you've already got an exact match for \r\n    \/\/this string in the m_valueMap\r\n    if (query.length() &gt; 0 &amp;&amp; m_valueMap.get(query) == null) {\r\n        updateFormFeedback(FormFeedback.LOADING, null);               \r\n                                    \r\n        queryOptions( \r\n                query,\r\n                m_indexFrom,\r\n                m_indexTo,\r\n            new RestSuggestCallback(m_request, m_callback, query));\r\n    }\r\n}\r\n<\/pre>\n<h4>queryOptions<\/h4>\n<p>The <code>queryOptions<\/code><span class=\"footnote\"><a name=\"suggnote_note13\" href=\"#suggnote_code13\">13<\/a><\/span> method sends a GET request to the URL of the REST endpoint passed to the constructor of <code>MultivalueSuggestBox<\/code><span class=\"footnote\"><a href=\"#suggnote_code1\">1<\/a><\/span>.  It uses the GWT <a href=\"http:\/\/google-web-toolkit.googlecode.com\/svn\/javadoc\/2.0\/com\/google\/gwt\/http\/client\/RequestBuilder.html\"><code>RequestBuilder<\/code><\/a> object.  <code>queryOptions<\/code> converts the response from <a href=\"http:\/\/en.wikipedia.org\/wiki\/Json\">JSON<\/a> to <a href=\"http:\/\/en.wikipedia.org\/wiki\/Type_safety\">type safe<\/a> Java beans<span class=\"footnote\"><a href=\"#suggnote_code14\" name=\"suggnote_note14\">14<\/a><\/span>.  The arguments are the query string, the indices of the result set desired, and an <code>OptionQueryCallback<\/code><span class=\"footnote\"><a name=\"suggnote_note15\" href=\"#suggnote_code15\">15<\/a><\/span>.  <\/p>\n<pre>\r\n\/**\r\n * Retrieve Options (name-value pairs) that are suggested from the REST endpoint\r\n * @param query - the String search term \r\n * @param from - the 0-based begin index int\r\n * @param to - the end index inclusive int\r\n * @param callback - the OptionQueryCallback to handle the response\r\n *\/\r\nprivate void queryOptions(final String query, final int from, final int to, \r\n                          final OptionQueryCallback callback)<span class=\"footnote\"><a name=\"suggnote_code13\" href=\"#suggnote_note13\">13<\/a><\/span>\r\n{\r\n    RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, \r\n        URL.encode(m_restEndpointUrl + \"?q=\" + query + \"&amp;indexFrom=\" + \r\n        from + \"&amp;indexTo=\" + to));\r\n        \r\n    \/\/ Set our headers\r\n    builder.setHeader(\"Accept\", \"application\/json\");\r\n    builder.setHeader(\"Accept-Charset\", \"UTF-8\");\r\n                \r\n    builder.setCallback(new RequestCallback() {\r\n            \r\n        @Override\r\n        public void onResponseReceived(com.google.gwt.http.client.Request request, \r\n                                       Response response)\r\n        {\r\n            JSONValue val = JSONParser.parse(response.getText());\r\n            JSONObject obj = val.isObject();\r\n            int totSize = (int) obj.get(OptionResultSet.TOTAL_SIZE).isNumber().doubleValue();\r\n            OptionResultSet options = new OptionResultSet(totSize);<span class=\"footnote\"><a href=\"#suggnote_note14\" name=\"suggnote_code14\">14<\/a><\/span>\r\n            JSONArray optionsArray = obj.get(OptionResultSet.OPTIONS).isArray();\r\n\r\n            if (options.getTotalSize() &gt; 0 &amp;&amp; optionsArray != null) {\r\n                    \r\n                for (int i = 0; i &lt; optionsArray.size(); i++) {                        \r\n                    JSONObject jsonOpt = optionsArray.get(i).isObject();\r\n                    Option option = new Option();\r\n                    option.setName(jsonOpt.get(OptionResultSet.DISPLAY_NAME).isString().stringValue());\r\n                    option.setValue(jsonOpt.get(OptionResultSet.VALUE).isString().stringValue());\r\n                    options.addOption(option);\r\n                }\r\n            }                    \r\n            callback.success(options);\r\n        }\r\n\r\n        @Override\r\n        public void onError(com.google.gwt.http.client.Request request, \r\n                            Throwable exception)\r\n        {\r\n            callback.error(exception);\r\n        }\r\n    });\r\n        \r\n    try {\r\n        builder.send();\r\n    } catch (RequestException e) {\r\n        updateFormFeedback(FormFeedback.ERROR, \"Error: \" + e.getMessage());\r\n    }\r\n}\r\n<\/pre>\n<\/p>\n<h4>OptionQueryCallback<\/h4>\n<p>The <code>OptionQueryCallback<\/code> abstract class handles success and error conditions from the REST call.<\/p>\n<pre>\r\n\/**\r\n * Handles success and error conditions from the REST call\r\n *\/\r\nprivate abstract class OptionQueryCallback<span class=\"footnote\"><a name=\"suggnote_code15\" href=\"#suggnote_note15\">15<\/a><\/span>\r\n{\r\n    abstract void success(OptionResultSet optResults);\r\n    abstract void error(Throwable exception);\r\n}\r\n<\/pre>\n<\/p>\n<h4>RestSuggestCallback<\/h4>\n<p><code>RestSuggestCallback<\/code><span class=\"footnote\"><a name=\"suggnote_note16\" href=\"#suggnote_code16\">16<\/a><\/span> extends <code>OptionQueryCallback<\/code><span class=\"footnote\"><a name=\"suggnote_note15\" href=\"#suggnote_code15\">15<\/a><\/span> and is the object passed to <code>queryOptions<\/code><span class=\"footnote\"><a href=\"#suggnote_code13\">13<\/a><\/span> from <code>getSuggestions<\/code><span class=\"footnote\"><a href=\"#suggnote_code12\">12<\/a><\/span>.  The <code>RestSuggestCallback<\/code> constructor takes the <a href=\"http:\/\/google-web-toolkit.googlecode.com\/svn\/javadoc\/2.0\/com\/google\/gwt\/user\/client\/ui\/SuggestOracle.Request.html\"><code>SuggestOracle.Request<\/code><\/a>, <a href=\"http:\/\/google-web-toolkit.googlecode.com\/svn\/javadoc\/2.0\/com\/google\/gwt\/user\/client\/ui\/SuggestOracle.Callback.html\"><code>SuggestOracle.Callback<\/code><\/a>, and the query as arguments.\n<\/p>\n<p>\nOnce the request succeeds <code>RestSuggestCallback<\/code> evaluates the response:\n<\/p>\n<ul>\n<li>if the total size is less than one, it means there were no suggestions returned from the REST endpoint, so the <code>FormFeedback<\/code> shows an <!-- Zack: the form feedback icons are really small.  You could just embed them right in the text here instead of saying ERROR --><code>ERROR<\/code> status.<span class=\"footnote\"><a name=\"suggnote_note17\" href=\"#suggnote_code17\">17<\/a><\/span>  The <code>OptionSuggestion<\/code> <code>ArrayList<\/code> remains empty.<span class=\"footnote\"><a name=\"suggnote_note18\" href=\"#suggnote_code18\">18<\/a><\/span><\/li>\n<li>if the total size is equal to one, then there was only one suggestion.  In this case we skip the popup and just show the value in the text field.  The name of the <code>Option<\/code> replaces the string after the last <code>DISPLAY_SEPARATOR<\/code>  <span class=\"footnote\"><a name=\"suggnote_note19\" href=\"#suggnote_code19\">19<\/a><\/span>.   The  <code>FormFeedback<\/code> shows a <!-- Zack: you can use the icon here too --><code>VALID<\/code> status<span class=\"footnote\"><a name=\"suggnote_note20\" href=\"#suggnote_code20\">20<\/a><\/span>.  The name and value are added to the value map.<span class=\"footnote\"><a name=\"suggnote_note21\" href=\"#suggnote_code21\">21<\/a><\/span>.   The <code>OptionSuggestion<\/code> <code>ArrayList<\/code> remains empty.<span class=\"footnote\"><a name=\"suggnote_note18\" href=\"#suggnote_code18\">18<\/a><\/span><\/li>\n<li>if the total size is greater than 1, the The <code>OptionSuggestion<\/code> <code>ArrayList<\/code> is built up<span class=\"footnote\"><a name=\"suggnote_note22\" href=\"#suggnote_code22\">22<\/a><\/span> and these suggestions are sent back to the <code>SuggestOracle.Callback<\/code><span class=\"footnote\"><a name=\"suggnote_note23\" href=\"#suggnote_code23\">23<\/a><\/span>. We also conditionally add the next and previous options for scrolling in the results.<span class=\"footnote\"><a name=\"suggnote_note24\" href=\"#suggnote_code24\">24<\/a><\/span>  The <code>FormFeedback<\/code> shows an <!-- Zack: and another icon --><code>ERROR<\/code> status until the user selects a valid option.<span class=\"footnote\"><a name=\"suggnote_note25\" href=\"#suggnote_code25\">25<\/a><\/span>  <\/li>\n<\/ul>\n<pre>\r\n\/**\r\n * A custom callback that has the original SuggestOracle.Request \r\n * and SuggestOracle.Callback\r\n *\/\r\nprivate class RestSuggestCallback extends OptionQueryCallback<span class=\"footnote\"><a name=\"suggnote_code16\" href=\"#suggnote_note16\">16<\/a><\/span>\r\n{\r\n    private SuggestOracle.Request m_request;\r\n    private SuggestOracle.Callback m_callback;\r\n    private String m_query; \/\/this may be different from m_request.getQuery when multivalued it's only the substring after the last delimiter\r\n        \r\n    RestSuggestCallback(Request request, Callback callback, String query)\r\n    {\r\n        m_request = request;\r\n        m_callback = callback;\r\n        m_query = query;\r\n    }\r\n\r\n    public void success(OptionResultSet optResults)\r\n    {\r\n        SuggestOracle.Response resp = new SuggestOracle.Response();\r\n        List&lt;OptionSuggestion&gt; suggs = new ArrayList&lt;OptionSuggestion&gt;();<span class=\"footnote\"><a name=\"suggnote_code18\" href=\"#suggnote_note18\">18<\/a><\/span>\r\n        int totSize = optResults.getTotalSize();\r\n            \r\n        if (totSize &lt; 1) {<span class=\"footnote\"><a name=\"suggnote_code16\" href=\"#suggnote_note16\">16<\/a><\/span>\r\n            \/\/if there were no suggestions, then it's an invalid value\r\n            updateFormFeedback(FormFeedback.ERROR, \"Invalid: \" + query);<span class=\"footnote\"><a name=\"suggnote_code17\" href=\"#suggnote_note17\">17<\/a><\/span>\r\n                \r\n        } else if (totSize == 1) {\r\n            \/\/it's an exact match, so do not bother with showing suggestions, \r\n            Option o = optResults.getOptions()[0];\r\n            String displ = o.getName();\r\n                \r\n            \/\/remove the last bit up to separator\r\n            m_field.setText(getFullReplaceText(displ, m_request.getQuery()));<span class=\"footnote\"><a name=\"suggnote_code19\" href=\"#suggnote_note19\">19<\/a><\/span>\r\n                \r\n            \/\/it's valid!\r\n            updateFormFeedback(FormFeedback.VALID, null);<span class=\"footnote\"><a name=\"suggnote_code20\" href=\"#suggnote_note20\">20<\/a><\/span>\r\n\r\n            \/\/set the value into the valueMap\r\n            putValue(displ, o.getValue());<span class=\"footnote\"><a name=\"suggnote_code21\" href=\"#suggnote_note21\">21<\/a><\/span>\r\n\r\n        } else {\r\n            \/\/more than 1 so show the suggestions\r\n                \r\n            \/\/if not at the first page, show PREVIOUS\r\n            if (m_indexFrom &gt; 0) {\r\n                OptionSuggestion prev = new OptionSuggestion(\r\n                    OptionSuggestion.PREVIOUS_VALUE, m_request.getQuery());\r\n                suggs.add(prev);\r\n            }\r\n                \r\n            \/\/ show the suggestions\r\n            for (Option o : optResults.getOptions()) {\r\n                OptionSuggestion sugg = new OptionSuggestion(\r\n\t            o.getName(), o.getValue(), m_request.getQuery(), m_query);\r\n                suggs.add(sugg);<span class=\"footnote\"><a name=\"suggnote_code22\" href=\"#suggnote_note22\">22<\/a><\/span>\r\n            }\r\n                \r\n            \/\/if there are more pages, show NEXT\r\n            if (m_indexTo &lt; totSize) {\r\n                OptionSuggestion next = new OptionSuggestion(\r\n                    OptionSuggestion.NEXT_VALUE, m_request.getQuery());\r\n                suggs.add(next);<span class=\"footnote\"><a name=\"suggnote_code24\" href=\"#suggnote_note24\">24<\/a><\/span>\r\n            }\r\n                \r\n            \/\/nothing has been picked yet, so let the feedback show an error (unsaveable)\r\n            updateFormFeedback(FormFeedback.ERROR, \"Invalid: \" + m_query);<span class=\"footnote\"><a name=\"suggnote_code25\" href=\"#suggnote_note25\">25<\/a><\/span>\r\n        }\r\n\r\n        \/\/it's ok (and good) to pass an empty suggestion list back to the suggest box's callback method\r\n        \/\/the list is not shown at all if the list is empty.\r\n        resp.setSuggestions(suggs);\r\n        m_callback.onSuggestionsReady(m_request, resp);<span class=\"footnote\"><a name=\"suggnote_code23\" href=\"#suggnote_note23\">23<\/a><\/span>\r\n    }\r\n\r\n    @Override\r\n    public void error(Throwable exception)\r\n    {\r\n        updateFormFeedback(FormFeedback.ERROR, \"Invalid: \" + m_query);\r\n    }    \r\n}\r\n<\/pre>\n<\/p>\n<h4>OptionSuggestion<\/h4>\n<p><code>OptionSuggestion<\/code> objects are collected into a <code>List<\/code> and passed to <code>SuggestOracle.Callback.onSuggestionsReady<\/code><span class=\"footnote\"><a href=\"#suggnote_code24\">24<\/a><\/span>.  It extends <a href=\"http:\/\/google-web-toolkit.googlecode.com\/svn\/javadoc\/2.0\/com\/google\/gwt\/user\/client\/ui\/SuggestOracle.Suggestion.html\"><code>SuggestOracle.Suggestion<\/code><\/a> overriding <code>getDisplayString<\/code> and <code>getReplacementString<\/code> and adding <code>getValue<\/code> and <code>getName<\/code> methods for <code>Option<\/code> IDs and names.  You could add more properties depending on your needs.  In this example, the names are the crayon colors and the values are their corresponding hex codes.<\/p>\n<p><code>OptionSuggestion<\/code> has two constructors.  One is for navigation<span class=\"footnote\"><a name=\"suggnote_note26\" href=\"#suggnote_code26\">26<\/a><\/span> and the other is for an actual option<span class=\"footnote\"><a name=\"suggnote_note27\" href=\"#suggnote_code27\">27<\/a><\/span>.\n<\/p>\n<p>\nSince <code>RestSuggestOracle.isDisplayStringHTML<\/code> is true, HTML can be used for the display string.  In the body of the first constructor, the HTML returned is a div with a class that is included in the css.  It will show an appropriate image to indicate navigation.  In the body of the second constructor, HTML bold tags are used to highlight the query search term within each suggestion in the list popup.  The value returned by <code>getReplacementString<\/code><span class=\"footnote\"><a name=\"suggnote_note28\" href=\"#suggnote_code28\">28<\/a><\/span> is determined by calling <code>getFullReplaceText<\/code> so that the text box does not lose any previously selected options in the multi-value case, and only the portion of the contents of the text field after the last <code>DISPLAY_SEPARATOR<\/code> is replaced.<\/p>\n<pre>    \r\n\/**\r\n * Constructor for navigation options\r\n * @param nav - next or previous value\r\n * @param currentTextValue - the current contents of the text box\r\n *\/    \r\nOptionSuggestion(String nav, String currentTextValue)<span class=\"footnote\"><a name=\"suggnote_code26\" href=\"#suggnote_note26\">26<\/a><\/span>\r\n{\r\n    if (NEXT_VALUE.equals(nav)) {\r\n        m_display = \"&lt;div class=\\\"autocompleterNext\\\" title=\\\"Next\\\"&gt;&lt;\/div&gt;\";\r\n    } else {\r\n        m_display = \"&lt;div class=\\\"autocompleterPrev\\\" title=\\\"Previous\\\"&gt;&lt;\/div&gt;\";\r\n    }\r\n    m_replace = currentTextValue;\r\n    m_value = nav;\r\n}\r\n    \r\n\/**\r\n * Constructor for regular options\r\n * @param displ - the name of the option\r\n * @param val - the value of the option\r\n * @param replacePre - the current contents of the text box\r\n * @param query - the query\r\n *\/\r\nOptionSuggestion(String displ, String val, String replacePre, String query)<span class=\"footnote\"><a name=\"suggnote_code27\" href=\"#suggnote_note27\">27<\/a><\/span>\r\n{\r\n    m_name = displ;\r\n    int begin = displ.toLowerCase().indexOf(query.toLowerCase());\r\n    if (begin &gt;= 0) {\r\n        int end = begin + query.length();\r\n        String match = displ.substring(begin, end);\r\n        m_display = displ.replaceFirst(match, \"&lt;b&gt;\" + match + \"&lt;\/b&gt;\");\r\n    } else {\r\n        \/\/may not necessarily be a part of the query, for example if \"*\" was typed.\r\n        m_display = displ;\r\n    }\r\n    m_replace = getFullReplaceText(displ, replacePre);<span class=\"footnote\"><a name=\"suggnote_code28\" href=\"#suggnote_note28\">28<\/a><\/span>\r\n    m_value = val;\r\n}    \r\n<\/pre>\n<\/p>\n<h4>onSelection<\/h4>\n<p><code>MultivalueSuggestBox<\/code> implements <a href=\"http:\/\/google-web-toolkit.googlecode.com\/svn\/javadoc\/2.0\/com\/google\/gwt\/event\/logical\/shared\/SelectionHandler.html\"><code>SelectionHandler&lt;Suggestion&gt;<\/code><\/a>, so <code>onSelection<\/code><span class=\"footnote\"><a name=\"suggnote_note29\" href=\"#suggnote_code29\">29<\/a><\/span> is called after the user chooses an option among the items shown in the <code>SuggestBox<\/code> list popup.  If an option is selected, the <code>FormFeedback<\/code> shows a <!-- Zack: another icon here --><code>VALID<\/code> status and the option&#8217;s name and value are put<span class=\"footnote\"><a name=\"suggnote_note30\" href=\"#suggnote_code30\">30<\/a><\/span> into the value map.  If the selection is one of the navigation options, the indices are changed and <code>getSuggestions<\/code> is called again<span class=\"footnote\"><a name=\"suggnote_note31\" href=\"#suggnote_code31\">31<\/a><\/span>.  Having the next\/previous options allows for browsing of the result set within a reasonably sized popup that does not need scrollbars.<\/p>\n<pre>\r\n@Override\r\npublic void onSelection(SelectionEvent&lt;Suggestion&gt; event)<span class=\"footnote\"><a name=\"suggnote_code29\" href=\"#suggnote_note29\">29<\/a><\/span>\r\n{\r\n    Suggestion suggestion = event.getSelectedItem();\r\n    if (suggestion instanceof OptionSuggestion) {\r\n        OptionSuggestion osugg = (OptionSuggestion) suggestion;\r\n        \/\/if NEXT or PREVIOUS were selected, requery but bypass the timer\r\n        String value = osugg.getValue();\r\n        if (OptionSuggestion.NEXT_VALUE.equals(value)) {\r\n            m_indexFrom += PAGE_Size;\r\n            m_indexTo += PAGE_Size;\r\n            \r\n            RestSuggestOracle oracle = (RestSuggestOracle) m_field.getSuggestOracle();\r\n            oracle.getSuggestions();<span class=\"footnote\"><a name=\"suggnote_code31\" href=\"#suggnote_note31\">31<\/a><\/span>\r\n            \r\n        } else if (OptionSuggestion.PREVIOUS_VALUE.equals(value)) {\r\n            m_indexFrom -= PAGE_Size;\r\n            m_indexTo -= PAGE_Size;\r\n            \r\n            RestSuggestOracle oracle = (RestSuggestOracle) m_field.getSuggestOracle();\r\n            oracle.getSuggestions();\r\n            \r\n        } else {\r\n            \/\/made a valid selection\r\n            updateFormFeedback(FormFeedback.VALID, null);\r\n            \r\n            \/\/add the option's value to the value map            \r\n            putValue(osugg.getName(), value);<span class=\"footnote\"><a name=\"suggnote_code30\" href=\"#suggnote_note30\">30<\/a><\/span>\r\n            \r\n            \/\/put the focus back into the textfield so user\r\n            \/\/can enter more\r\n            m_field.setFocus(true);\r\n        }\r\n    }\r\n}\r\n\r\n<\/pre>\n<\/p>\n<h4>findExactMatches<\/h4>\n<p>Now let&#8217;s go back and look at <code>findExactMatches<\/code><span class=\"footnote\"><a name=\"suggnote_note32\" href=\"#suggnote_code32\">32<\/a><\/span>.  This is only called if multi-valued was specified, and is useful because the user might want to copy and paste an entire set of names or need to change a name that is in the middle of the text field.  <!--insert image here? -->  If there is more than one name in the text field, a query is executed for every name that does not already have a value in the value map.  It resets the member variables <code>m_findExactMatchesTotal<\/code>, <code>m_findExactMatchesFound<\/code>, and <code>m_findExactMatchesNot<\/code><span class=\"footnote\"><a name=\"suggnote_note33\" href=\"#suggnote_code33\">33<\/a><\/span>, then for every non-valued term it calls <code>findExactMatch<\/code><span class=\"footnote\"><a name=\"suggnote_note34\" href=\"#suggnote_code34\">34<\/a><\/span>.<\/p>\n<pre>\r\n\/**\r\n * If there is more than one key in the text field,\r\n * check that every key has a value in the map.\r\n * For any that do not, try to find its exact match.\r\n *\/\r\nprivate void findExactMatches()<span class=\"footnote\"><a name=\"suggnote_code32\" href=\"#suggnote_note32\">32<\/a><\/span>\r\n{\r\n    String text = m_field.getText();\r\n    String[] keys = text.split(DISPLAY_SEPARATOR.trim());\r\n    int len = keys.length;       \r\n    if (len &lt; 2) {\r\n        \/\/do not continue.  if there&#039;s 1, it is the last one, and getSuggestions can handle it\r\n        return;\r\n    }\r\n\r\n    m_findExactMatchesTotal = 0;\r\n    m_findExactMatchesFound = 0;\r\n    m_findExactMatchesNot.clear();<span class=\"footnote\"><a name=\"suggnote_code33\" href=\"#suggnote_note33\">33<\/a><\/span>\r\n    for (int pos = 0; pos &lt; len; pos++) {\r\n        String key = keys[pos].trim();\r\n\r\n        if (!key.isEmpty()) {\r\n            String v = m_valueMap.get(key);\r\n            if (null == v) {\r\n                m_findExactMatchesTotal++;\r\n            }\r\n        }\r\n    }\r\n    \/\/then loop through again and try to find them\r\n    \/*\r\n     * We may have invalid values due to a multi-value copy-n-paste,\r\n     * or going back and messing with a middle or first key;\r\n     * so for each invalid value, try to find an exact match.\r\n     *\/\r\n    for (int pos = 0; pos &lt; len; pos++) {\r\n        String key = keys[pos].trim();\r\n        if (!key.isEmpty()) {\r\n            String v = m_valueMap.get(key);\r\n            if (null == v) {\r\n                findExactMatch(key, pos);<span class=\"footnote\"><a name=\"suggnote_code34\" href=\"#suggnote_note34\">34<\/a><\/span>\r\n            }\r\n        }\r\n    }        \r\n}\r\n<\/pre>\n<\/p>\n<h4>findExactMatch<\/h4>\n<p><code>findExactMatch<\/code><span class=\"footnote\"><a name=\"suggnote_note35\" href=\"#suggnote_code35\">35<\/a><\/span> updates the <code>FormFeedback<\/code> to show a <!-- Zack: another icon --><code>LOADING<\/code> status.  It then calls <code>queryOptions<\/code><span class=\"footnote\"><a href=\"#suggnote_code13\">13<\/a><\/span>, but this time the indices are hard-coded from zero to some relatively small amount so the exact match can attempt to be found within the top suggestions.\n<\/p>\n<p>\nAn anonymous <code>OptionQueryCallback<\/code><span class=\"footnote\"><a name=\"suggnote_note36\" href=\"#suggnote_code36\">36<\/a><\/span> evaluates the response:\n<\/p>\n<ul>\n<li>if the total size is equal to one, then the one option is the match, so the name and value will be placed in the value map and <code>m_findExactMatchesFound<\/code> is incremented.<span class=\"footnote\"><a name=\"suggnote_note37\" href=\"#suggnote_code37\">37<\/a><\/span><\/li>\n<li>if the total size is greater than one, it loops through the result set to find if there is an exact match within those top suggestions, and if so, the name and value will be placed in the value map and <code>m_findExactMatchesFound<\/code> is incremented.<span class=\"footnote\"><a name=\"suggnote_note38\" href=\"#suggnote_code38\">38<\/a><\/span><\/li>\n<li>if an exact match is not found, then the term is added to the <code>m_findExactMatchesNot<\/code> list.<span class=\"footnote\"><a name=\"suggnote_note39\" href=\"#suggnote_code39\">39<\/a><\/span><\/li>\n<\/ul>\n<p>Once <code>m_findExactMatchesFound + m_findExactMatchesNot.size()<\/code> is equal to <code>m_findExactMatchesTotal<\/code>, the <code>FormFeedback<\/code> will be updated to either show <!-- Zack: another icon --><code>VALID<\/code> if all exact matches were found<span class=\"footnote\"><a name=\"suggnote_note40\" href=\"#suggnote_code40\">40<\/a><\/span> or <!-- Zack: another icon --><code>ERROR<\/code> if any name could not be exactly matched.  All names that could not be matched will be shown within the tooltip (i.e., title) of the <code>FormFeedback<\/code>.<span class=\"footnote\"><a name=\"suggnote_note41\" href=\"#suggnote_code41\">41<\/a><\/span><\/p>\n<pre>\r\nprivate void findExactMatch(final String displayValue, final int position)<span class=\"footnote\"><a name=\"suggnote_code35\" href=\"#suggnote_note35\">35<\/a><\/span>\r\n{\r\n    updateFormFeedback(FormFeedback.LOADING, null);\r\n    \r\n    queryOptions( \r\n        displayValue,\r\n            0,\r\n            FIND_EXACT_MATCH_QUERY_LIMIT, \r\n        new OptionQueryCallback() {<span class=\"footnote\"><a name=\"suggnote_code36\" href=\"#suggnote_note36\">36<\/a><\/span>\r\n            \r\n            @Override\r\n            public void error(Throwable exception)\r\n            {\r\n                \/\/ an exact match couldn't be found, just increment not found\r\n                m_findExactMatchesNot.add(displayValue);\r\n                finalizeFindExactMatches();\r\n            }\r\n\r\n            @Override\r\n            public void success(OptionResultSet optResults)\r\n            {\r\n                int totSize = optResults.getTotalSize();\r\n                if (totSize == 1) {\r\n                    \/\/an exact match was found, so place it in the value map\r\n                    Option option = optResults.getOptions()[0];                        \r\n                    extactMatchFound(position, option);<span class=\"footnote\"><a name=\"suggnote_code37\" href=\"#suggnote_note37\">37<\/a><\/span>\r\n                } else {\r\n                    \/\/try to find the exact matches within the results\r\n                    boolean found = false;\r\n                    for (Option option : optResults.getOptions()) {\r\n                        if (displayValue.equalsIgnoreCase(option.getName())) {\r\n                            extactMatchFound(position, option);<span class=\"footnote\"><a name=\"suggnote_code38\" href=\"#suggnote_note38\">38<\/a><\/span>\r\n                            found = true;\r\n                            break;\r\n                        }                            \r\n                    }\r\n                    if (!found) {\r\n                        m_findExactMatchesNot.add(displayValue);<span class=\"footnote\"><a name=\"suggnote_code39\" href=\"#suggnote_note39\">39<\/a><\/span>\r\n                    }\r\n                }\r\n                finalizeFindExactMatches();                    \r\n            }\r\n\r\n            private void extactMatchFound(final int position, Option option)\r\n            {\r\n                putValue(option.getName(), option.getValue());\r\n\r\n                \/\/and replace the text\r\n                String text = m_field.getText();\r\n                String[] keys = text.split(DISPLAY_SEPARATOR.trim());\r\n                keys[position] = option.getName();\r\n                String join = \"\";\r\n                for (String n : keys) {\r\n                    join += n.trim() + DISPLAY_SEPARATOR;\r\n                }\r\n                join = trimLastDelimiter(join, DISPLAY_SEPARATOR);\r\n                m_field.setText(join);\r\n                \r\n                m_findExactMatchesFound++;\r\n            }\r\n            \r\n            private void finalizeFindExactMatches()\r\n            {\r\n                if (m_findExactMatchesFound + m_findExactMatchesNot.size() == \r\n                        m_findExactMatchesTotal) {\r\n                    \/\/when the found + not = total, we're done\r\n                    if (m_findExactMatchesNot.size() &gt; 0) {\r\n                        String join = \"\";\r\n                        for (String val : m_findExactMatchesNot) {\r\n                            join += val.trim() + DISPLAY_SEPARATOR;\r\n                        }\r\n                        join = trimLastDelimiter(join, DISPLAY_SEPARATOR);                                \r\n                        updateFormFeedback(FormFeedback.ERROR, \"Invalid:\" + join);<span class=\"footnote\"><a name=\"suggnote_code41\" href=\"#suggnote_note41\">41<\/a><\/span>\r\n                    } else {\r\n                        updateFormFeedback(FormFeedback.VALID, null);<span class=\"footnote\"><a name=\"suggnote_code40\" href=\"#suggnote_note40\">40<\/a><\/span>\r\n                    }\r\n                }\r\n            }\r\n        });\r\n}\r\n<\/pre>\n<\/p>\n<p>\nYou can call <code>getValue<\/code><span class=\"footnote\"><a name=\"suggnote_note42\" href=\"#suggnote_code42\">42<\/a><\/span> when the values of the selected options are needed.  This implementation of getValue returns a concatenated string of all the values delimited by <code>VALUE_DELIM<\/code>, in this case a semi-colon.  If any of the names were found to be invalid, that is, never exactly matched or chosen, it is removed<span class=\"footnote\"><a name=\"suggnote_note43\" href=\"#suggnote_code43\">43<\/a><\/span> and the <code>FormFeedback<\/code> will show an <code>ERROR<\/code> status with a tooltip containing the invalid name(s)<span class=\"footnote\"><a name=\"suggnote_note44\" href=\"#suggnote_code44\">44<\/a><\/span>.  Depending on your needs, you might choose to return the value map or an array of names and values if order is important.<\/p>\n<pre>\r\n\/**\r\n * Get the value(s) as a concatenated String\r\n * @return value(s) as a String\r\n *\/\r\npublic String getValue()<span class=\"footnote\"><a name=\"suggnote_code42\" href=\"#suggnote_note42\">42<\/a><\/span>\r\n{\r\n    \/\/String together all the values in the valueMap\r\n    \/\/based on the display values shown in the field\r\n    String text = m_field.getText();\r\n    \r\n    String values = \"\";\r\n    String invalids = \"\";\r\n    String newKeys = \"\";\r\n    if (m_isMultivalued) {\r\n        String[] keys = text.split(DISPLAY_SEPARATOR);\r\n        for (String key : keys) {\r\n            key = key.trim();\r\n            if (!key.isEmpty()) {\r\n                String v = m_valueMap.get(key);\r\n                if (null != v) {\r\n                    values += v + VALUE_DELIM;\r\n                    \/\/rebuild newKeys removing invalids\r\n                    newKeys += key + DISPLAY_SEPARATOR;<span class=\"footnote\"><a name=\"suggnote_code43\" href=\"#suggnote_note43\">43<\/a><\/span>\r\n                } else {\r\n                    invalids += key + DISPLAY_SEPARATOR;\r\n                }\r\n            }\r\n        }\r\n        values = trimLastDelimiter(values, VALUE_DELIM);\r\n        \/\/set the new display values\r\n        m_field.setText(newKeys);\r\n    } else {\r\n        values = m_valueMap.get(text);\r\n    }\r\n    \r\n    \/\/if there were any invalid show warning\r\n    if (!invalids.isEmpty()) {\r\n        \/\/trim last separator\r\n        invalids = trimLastDelimiter(invalids, DISPLAY_SEPARATOR);\r\n        updateFormFeedback(FormFeedback.ERROR, \"Invalids: \" + invalids);<span class=\"footnote\"><a name=\"suggnote_code44\" href=\"#suggnote_note44\">44<\/a><\/span>\r\n    }\r\n    return values;\r\n}\r\n<\/pre>\n<\/p>\n<h2>The Takeaway<\/h2>\n<p>\nThis type-ahead control not only accepts multiple values but also auto-completes for a multi-valued copy and paste.  The user-friendly interface doesn&#8217;t impose a minimum number of characters before sending a request because it can limit the results shown while still allowing a complete browse of the dataset.\n<\/p>\n<p>\nYou could do all of that without REST, but we keep the client code completely encapsulated with a REST interface.  This sample could be extended with client-side caching to speed up performance.\n<\/p>\n<p>\nThe code in this example is free and released under the Apache 2.0 license.  The other programs needed to run this example are also free, but some of them may use different licenses. Make sure to read and understand each license before using a tool.\n<\/p>\n<p>\nWe encourage you to use this example in your own applications and we&#8217;d love to see the results.  Drop us a comment if you do.\n<\/p>\n<p style=\"margin-top: 5em\"><a name=\"bess\">Bess Siegal<\/a> is a software engineer at Novell.  She enjoys her <a href=\"\/blog\">one-minute commute<\/a> so she can spend more time with her husband and 3 daughters. <\/p>\n","protected":false},"excerpt":{"rendered":"<p>This is a guest post written by Bess Siegal.  Bess and I work together and she recently created an auto-complete field that handles multiple values.  She stopped by to show us how it works, provide an open source example, and give you all the details you need to create a multi-value auto-complete field in <a href=\"http:\/\/code.google.com\/webtoolkit\/\">GWT<\/a> using <a href=\"\/hackito\/gwt-rest\/\">REST<\/a>.<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[22,20],"tags":[],"_links":{"self":[{"href":"http:\/\/www.zackgrossbart.com\/hackito\/wp-json\/wp\/v2\/posts\/419"}],"collection":[{"href":"http:\/\/www.zackgrossbart.com\/hackito\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/www.zackgrossbart.com\/hackito\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/www.zackgrossbart.com\/hackito\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"http:\/\/www.zackgrossbart.com\/hackito\/wp-json\/wp\/v2\/comments?post=419"}],"version-history":[{"count":179,"href":"http:\/\/www.zackgrossbart.com\/hackito\/wp-json\/wp\/v2\/posts\/419\/revisions"}],"predecessor-version":[{"id":613,"href":"http:\/\/www.zackgrossbart.com\/hackito\/wp-json\/wp\/v2\/posts\/419\/revisions\/613"}],"wp:attachment":[{"href":"http:\/\/www.zackgrossbart.com\/hackito\/wp-json\/wp\/v2\/media?parent=419"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/www.zackgrossbart.com\/hackito\/wp-json\/wp\/v2\/categories?post=419"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/www.zackgrossbart.com\/hackito\/wp-json\/wp\/v2\/tags?post=419"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}