cc.kune.core.client.sitebar.search.MultivalueSuggestBox.java Source code

Java tutorial

Introduction

Here is the source code for cc.kune.core.client.sitebar.search.MultivalueSuggestBox.java

Source

/*
 *
 * Copyright (C) 2007-2015 Licensed to the Comunes Association (CA) under
 * one or more contributor license agreements (see COPYRIGHT for details).
 * The CA licenses this file to you under the GNU Affero General Public
 * License version 3, (the "License"); you may not use this file except in
 * compliance with the License. This file is part of kune.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
/*******************************************************************************
 *
 * 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.
 *
 * Source: http://www.zackgrossbart.com/hackito/gwt-rest-auto/
 *
 ******************************************************************************/
package cc.kune.core.client.sitebar.search;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import cc.kune.common.client.log.Log;
import cc.kune.common.client.notify.NotifyUser;
import cc.kune.common.shared.i18n.I18n;
import cc.kune.common.shared.i18n.I18nTranslationService;
import cc.kune.core.shared.SearcherConstants;
import cc.kune.core.shared.dto.GroupType;

import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.event.logical.shared.SelectionEvent;
import com.google.gwt.event.logical.shared.SelectionHandler;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.http.client.RequestCallback;
import com.google.gwt.http.client.RequestException;
import com.google.gwt.http.client.Response;
import com.google.gwt.http.client.URL;
import com.google.gwt.json.client.JSONArray;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONParser;
import com.google.gwt.json.client.JSONValue;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Focusable;
import com.google.gwt.user.client.ui.SuggestBox;
import com.google.gwt.user.client.ui.SuggestOracle;
import com.google.gwt.user.client.ui.SuggestOracle.Callback;
import com.google.gwt.user.client.ui.SuggestOracle.Request;
import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
import com.google.gwt.user.client.ui.TextArea;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.TextBoxBase;

// TODO: Auto-generated Javadoc
/**
 * A SuggestBox that uses REST and allows for multiple values, autocomplete and
 * browsing.
 * 
 * @author Bess Siegal <bsiegal@novell.com>
 */
public class MultivalueSuggestBox extends Composite
        implements SelectionHandler<Suggestion>, Focusable, KeyUpHandler {

    /**
     * Bean for name-value pairs.
     * 
     * @author vjrj@ourproject.org (Vicente J. Ruiz Jurado)
     */
    private class Option {

        private String mname;
        private String mvalue;

        /**
         * No argument constructor
         */
        public Option() {
        }

        /**
         * @return Returns the name.
         */
        public String getName() {
            return mname;
        }

        /**
         * @return Returns the value.
         */
        public String getValue() {
            return mvalue;
        }

        /**
         * @param name
         *          The name to set.
         */
        public void setName(final String name) {
            mname = name;
        }

        /**
         * @param value
         *          The value to set.
         */
        public void setValue(final String value) {
            mvalue = value;
        }

    }

    /**
     * An abstract class that handles success and error conditions from the REST
     * call
     */
    private abstract class OptionQueryCallback {
        abstract void error(Throwable exception);

        abstract void success(OptionResultSet optResults);
    }

    /**
     * Bean for total size and options
     */
    protected class OptionResultSet {
        /** JSON key for DisplayName */
        public static final String DISPLAY_NAME = "longName";
        /** JSON key for Options */
        public static final String OPTIONS = "list";
        /** JSON key for the size of the Results */
        public static final String TOTAL_SIZE = "size";

        /** JSON key for Value */
        public static final String VALUE = "shortName";

        private final List<Option> m_options = new ArrayList<Option>();
        private int mtotalSize;

        /**
         * Constructor. Must pass in the total size.
         * 
         * @param totalSize
         *          the total size of the template
         */
        public OptionResultSet(final int totalSize) {
            setTotalSize(totalSize); // NOPMD by vjrj on 4/05/11 19:45
        }

        /**
         * Add an option
         * 
         * @param option
         *          - the Option to add
         */
        public void addOption(final Option option) {
            m_options.add(option);
        }

        /**
         * @return an array of Options
         */
        public Option[] getOptions() {
            return m_options.toArray(new Option[m_options.size()]);
        }

        /**
         * @return Returns the totalSize.
         */
        public int getTotalSize() {
            return mtotalSize;
        }

        /**
         * @param totalSize
         *          The totalSize to set.
         */
        public void setTotalSize(final int totalSize) {
            mtotalSize = totalSize;
        }
    }

    /**
     * A bean to serve as a custom suggestion so that the value is available and
     * the replace will look like it is supporting multivalues
     */
    class OptionSuggestion implements SuggestOracle.Suggestion {
        static final String NEXT_VALUE = "NEXT";
        static final String PREVIOUS_VALUE = "PREVIOUS";
        private String mdisplay;
        private String mname;
        private final String mreplace;
        private final String mvalue;

        /**
         * Constructor for navigation options
         * 
         * @param nav
         *          - next or previous value
         * @param currentTextValue
         *          - the current contents of the text box
         */
        OptionSuggestion(final String nav, final String currentTextValue) {
            if (NEXT_VALUE.equals(nav)) {
                mdisplay = "<div class=\"autocompleterNext\" title=\"Next\"></div>";
            } else {
                mdisplay = "<div class=\"autocompleterPrev\" title=\"Previous\"></div>";
            }
            mreplace = currentTextValue;
            mvalue = nav;
        }

        /**
         * Constructor for regular options
         * 
         * @param displ
         *          - the name of the option
         * @param val
         *          - the value of the option
         * @param replacePre
         *          - the current contents of the text box
         * @param query
         *          - the query
         */
        OptionSuggestion(final String displ, final String val, final String replacePre, final String query) {
            mname = displ;
            final int begin = displ.toLowerCase().indexOf(query.toLowerCase());
            if (begin >= 0) {
                final int end = begin + query.length();
                final String match = displ.substring(begin, end);
                mdisplay = displ.replaceFirst(match, "<b>" + match + "</b>");
            } else {
                // may not necessarily be a part of the query, for example if "*" was
                // typed.
                mdisplay = displ;
            }
            mreplace = getFullReplaceText(displ, replacePre);
            mvalue = val;
        }

        @Override
        public String getDisplayString() {
            return mdisplay;
        }

        /**
         * Get the name of the option. (when not multivalued, this will be the same
         * as getReplacementString)
         * 
         * @return name
         */
        public String getName() {
            return mname;
        }

        @Override
        public String getReplacementString() {
            return mreplace;
        }

        /**
         * Get the value of the option
         * 
         * @return value
         */
        public String getValue() {
            return mvalue;
        }
    }

    /**
     * A custom callback that has the original SuggestOracle.Request and
     * SuggestOracle.Callback
     */
    private class RestSuggestCallback extends OptionQueryCallback {
        private final SuggestOracle.Callback m_callback;
        private final String m_query; // this may be different from
                                      // m_request.getQuery when multivalued it's
                                      // only the substring after the last delimiter
        private final SuggestOracle.Request m_request;

        RestSuggestCallback(final Request request, final Callback callback, final String query) {
            m_request = request;
            m_callback = callback;
            m_query = query;
        }

        @Override
        public void error(final Throwable exception) {
            updateFormFeedback(FormFeedback.ERROR, "Invalid: " + m_query);
        }

        @Override
        public void success(final OptionResultSet optResults) {
            final SuggestOracle.Response resp = new SuggestOracle.Response();
            final List<OptionSuggestion> suggs = new ArrayList<OptionSuggestion>();
            final int totSize = optResults.getTotalSize();

            if (totSize < 1) {
                // if there were no suggestions, then it's an invalid value
                updateFormFeedback(FormFeedback.ERROR, "Invalid: " + m_query);
                if (showNoResult) {
                    final OptionSuggestion sugg = new OptionSuggestion(i18n.t("No results"), "#",
                            m_request.getQuery(), m_query);
                    suggs.add(sugg);
                }
            } else if (false && totSize == 1) {
                // Patch to show always the suggestions
                // it's an exact match, so do not bother with showing suggestions,
                final Option o = optResults.getOptions()[0];
                final String displ = o.getName();

                // remove the last bit up to separator
                // mfield.setText(getFullReplaceText(displ, m_request.getQuery()));

                Log.info("RestSuggestCallback.success! exact match found for displ = " + displ);
                // onExactMatch.onExactMatch(o.getValue());
                // it's valid!
                updateFormFeedback(FormFeedback.VALID, null);

                // set the value into the valueMap
                putValue(displ, o.getValue());

            } else {
                // more than 1 so show the suggestions

                // if not at the first page, show PREVIOUS
                if (mindexFrom > 0) {
                    final OptionSuggestion prev = new OptionSuggestion(OptionSuggestion.PREVIOUS_VALUE,
                            m_request.getQuery());
                    suggs.add(prev);
                }

                // show the suggestions
                for (final Option o : optResults.getOptions()) {
                    final OptionSuggestion sugg = new OptionSuggestion(o.getName(), o.getValue(),
                            m_request.getQuery(), m_query);
                    suggs.add(sugg);
                }

                // if there are more pages, show NEXT
                if (mindexTo < totSize) {
                    final OptionSuggestion next = new OptionSuggestion(OptionSuggestion.NEXT_VALUE,
                            m_request.getQuery());
                    suggs.add(next);
                }

                // nothing has been picked yet, so let the feedback show an error
                // (unsaveable)
                updateFormFeedback(FormFeedback.ERROR, "Invalid: " + m_query);
            }

            // it's ok (and good) to pass an empty suggestion list back to the suggest
            // box's callback method
            // the list is not shown at all if the list is empty.
            resp.setSuggestions(suggs);
            m_callback.onSuggestionsReady(m_request, resp);
        }

    }

    /*
     * Some custom inner classes for our SuggestOracle
     */
    /**
     * A custom Suggest Oracle
     */
    private class RestSuggestOracle extends SuggestOracle {
        private SuggestOracle.Callback mcallback;
        private SuggestOracle.Request mrequest;
        private final Timer mtimer;

        RestSuggestOracle() {
            mtimer = new Timer() {

                @Override
                public void run() {
                    /*
                     * The reason we check for empty string is found at
                     * http://development.lombardi.com/?p=39 -- paraphrased, if you
                     * backspace quickly the contents of the field are emptied but a query
                     * for a single character is still executed. Workaround for this is to
                     * check for an empty string field here.
                     */

                    if (!mfield.getText().trim().isEmpty()) {
                        if (misMultivalued) {
                            // calling this here in case a user is trying to correct the "kev"
                            // value of Allison Andrews, Kev, Josh Nolan or pasted in multiple
                            // values
                            findExactMatches();
                        }
                        getSuggestions();
                    }
                }
            };
        }

        private void getSuggestions() {
            String query = mrequest.getQuery();

            // find the last thing entered up to the last separator
            // and use that as the query
            if (misMultivalued) {
                final int sep = query.lastIndexOf(DISPLAY_SEPARATOR);
                if (sep > 0) {
                    query = query.substring(sep + DISPLAY_SEPARATOR.length());
                }
            }
            query = query.trim();

            // do not query if it's just an empty String
            // also do not get suggestions you've already got an exact match for this
            // string in the m_valueMap
            if (query.length() > 0 && mvalueMap.get(query) == null) {
                // JSUtil.println("getting Suggestions for: " + query);
                updateFormFeedback(FormFeedback.LOADING, null);
                queryOptions(query, mindexFrom, mindexTo, new RestSuggestCallback(mrequest, mcallback, query));
            }
        }

        @Override
        public boolean isDisplayStringHTML() {
            return true;
        }

        @Override
        public void requestSuggestions(final SuggestOracle.Request request, final SuggestOracle.Callback callback) {
            // This is the method that gets called by the SuggestBox whenever some
            // types into the text field
            mrequest = request;
            mcallback = callback;

            // reset the indexes (b/c NEXT and PREV call getSuggestions directly)
            resetPageIndices();

            // If the user keeps triggering this event (e.g., keeps typing), cancel
            // and restart the timer
            mtimer.cancel();
            mtimer.schedule(DELAY);
        }
    }

    private static final int DELAY = 500;

    private static final String DISPLAY_SEPARATOR = ", ";
    private static final int FIND_EXACT_MATCH_QUERY_LIMIT = 20;
    private static final int PAGE_SIZE = 15;
    private static final String VALUE_DELIM = ";";

    /**
     * Returns a String without the last delimiter
     * 
     * @param str
     *          - String to trim
     * @param delim
     *          - the delimiter
     * @return the String without the last delimter
     */
    private static String trimLastDelimiter(String str, final String delim) { // NOPMD
                                                                              // by
                                                                              // vjrj
                                                                              // on
                                                                              // 4/05/11
                                                                              // 19:46
        if (str.length() > 0) {
            str = str.substring(0, str.length() - delim.length());
        }
        return str;
    }

    private final I18nTranslationService i18n;
    private com.google.gwt.http.client.Request lastQuery;
    private final FormFeedback mfeedback;

    private final SuggestBox mfield;

    private int mfindExactMatchesFound = 0;

    private final ArrayList<String> mfindExactMatchesNot = new ArrayList<String>();

    private int mfindExactMatchesTotal = 0;

    private int mindexFrom = 0;

    private int mindexTo = 0;
    private boolean misMultivalued = false;

    private String mrestEndpointUrl;

    private final Map<String, String> mvalueMap;

    private final boolean showNoResult;

    // private final OnExactMatch onExactMatch;

    /**
     * Constructor.
     * 
     * @param i18n
     * 
     * @param the
     *          URL for the REST endpoint. This URL should accept the parameters q
     *          (for query), indexFrom and indexTo
     * @param isMultivalued
     *          - true for allowing multiple values
     * @param onExactMatch
     * @param showNoResult
     *          if we have to show noResult message when the search is empty or
     *          not
     */
    public MultivalueSuggestBox(final I18nTranslationService i18n, final boolean showNoResult,
            final String restEndpointUrl, final boolean isMultivalued, final OnExactMatch onExactMatch) {
        this.i18n = i18n;
        this.showNoResult = showNoResult;
        mrestEndpointUrl = restEndpointUrl;
        misMultivalued = isMultivalued;
        // this.onExactMatch = onExactMatch;

        final FlowPanel panel = new FlowPanel();
        TextBoxBase textfield;
        if (isMultivalued) {
            panel.addStyleName("textarearow");
            textfield = new TextArea();
        } else {
            panel.addStyleName("textfieldrow");
            textfield = new TextBox();
        }

        // Create our own SuggestOracle that queries REST endpoint
        final SuggestOracle oracle = new RestSuggestOracle();
        // intialize the SuggestBox
        mfield = new SuggestBox(oracle, textfield);
        if (isMultivalued) {
            // have to do this here b/c gwt suggest box wipes
            // style name if added in previous if
            textfield.addStyleName("multivalue");
        }
        mfield.addStyleName("wideTextField");
        mfield.addSelectionHandler(this);
        mfield.addKeyUpHandler(this);

        panel.add(mfield);
        mfeedback = new FormFeedback();
        // panel.add(mfeedback);

        initWidget(panel);

        /*
         * Create a Map that holds the values that should be stored. It will be
         * keyed on "display value", so that any time a "display value" is added or
         * removed the valueMap can be updated.
         */
        mvalueMap = new HashMap<String, String>();

        resetPageIndices();
    }

    // private final OnExactMatch onExactMatch;

    private void findExactMatch(final String displayValue, final int position) {
        updateFormFeedback(FormFeedback.LOADING, null);

        queryOptions(displayValue, 0, FIND_EXACT_MATCH_QUERY_LIMIT, // return a
                // relatively
                // small amount
                // in case
                // wanted "Red"
                // and
                // "Brick Red"
                // is the first
                // thing
                // returned
                new OptionQueryCallback() {

                    @Override
                    public void error(final Throwable exception) {
                        // an exact match couldn't be found, just increment not found
                        mfindExactMatchesNot.add(displayValue);
                        finalizeFindExactMatches();
                    }

                    private void extactMatchFound(final int position, final Option option) {
                        putValue(option.getName(), option.getValue());
                        Log.info("extactMatchFound ! exact match found for displ = " + displayValue);

                        // onExactMatch.onExactMatch(option.getValue());
                        // and replace the text
                        final String text = mfield.getText();
                        final String[] keys = text.split(DISPLAY_SEPARATOR.trim());
                        keys[position] = option.getName();
                        String join = "";
                        for (final String n : keys) {
                            join += n.trim() + DISPLAY_SEPARATOR;
                        }
                        join = trimLastDelimiter(join, DISPLAY_SEPARATOR);
                        // Commented mfield.setText(join);

                        mfindExactMatchesFound++;
                    }

                    private void finalizeFindExactMatches() {
                        if (mfindExactMatchesFound + mfindExactMatchesNot.size() == mfindExactMatchesTotal) {
                            // when the found + not = total, we're done
                            if (mfindExactMatchesNot.size() > 0) {
                                String join = "";
                                for (final String val : mfindExactMatchesNot) {
                                    join += val.trim() + DISPLAY_SEPARATOR;
                                }
                                join = trimLastDelimiter(join, DISPLAY_SEPARATOR);
                                updateFormFeedback(FormFeedback.ERROR, "Invalid:" + join);
                            } else {
                                updateFormFeedback(FormFeedback.VALID, null);
                            }
                        }
                    }

                    @Override
                    public void success(final OptionResultSet optResults) {
                        final int totSize = optResults.getTotalSize();
                        if (totSize == 1) {
                            // an exact match was found, so place it in the value map
                            final Option option = optResults.getOptions()[0];
                            extactMatchFound(position, option);
                        } else {
                            // try to find the exact matches within the results
                            boolean found = false;
                            for (final Option option : optResults.getOptions()) {
                                if (displayValue.equalsIgnoreCase(option.getName())) {
                                    extactMatchFound(position, option);
                                    found = true;
                                    break;
                                }
                            }
                            if (!found) {
                                mfindExactMatchesNot.add(displayValue);
                                Log.info("RestExactMatchCallback -- exact match not found for displ = "
                                        + displayValue);
                            }
                        }
                        finalizeFindExactMatches();
                    }
                });
    }

    /**
     * If there is more than one key in the text field, check that every key has a
     * value in the map. For any that do not, try to find its exact match.
     */
    private void findExactMatches() {
        final String text = mfield.getText();
        final String[] keys = text.split(DISPLAY_SEPARATOR.trim());
        final int len = keys.length;
        if (len < 2) {
            // do not continue. if there's 1, it is the last one, and getSuggestions
            // can handle it
            return;
        }

        mfindExactMatchesTotal = 0;
        mfindExactMatchesFound = 0;
        mfindExactMatchesNot.clear();
        for (int pos = 0; pos < len; pos++) {
            final String key = keys[pos].trim();

            if (!key.isEmpty()) {
                final String v = mvalueMap.get(key);
                if (null == v) {
                    mfindExactMatchesTotal++;
                }
            }
        }
        // then loop through again and try to find them
        /*
         * We may have invalid values due to a multi-value copy-n-paste, or going
         * back and messing with a middle or first key; so for each invalid value,
         * try to find an exact match. *
         */
        for (int pos = 0; pos < len; pos++) {
            final String key = keys[pos].trim();
            if (!key.isEmpty()) {
                final String v = mvalueMap.get(key);
                if (null == v) {
                    findExactMatch(key, pos);
                }
            }
        }
    }

    private String getFullReplaceText(final String displ, String replacePre) { // NOPMD
                                                                               // by
                                                                               // vjrj
                                                                               // on
                                                                               // 4/05/11
                                                                               // 19:45
                                                                               // replace the last bit after the last comma
        if (replacePre.lastIndexOf(DISPLAY_SEPARATOR) > 0) {
            replacePre = replacePre.substring(0, replacePre.lastIndexOf(DISPLAY_SEPARATOR)) + DISPLAY_SEPARATOR;
        } else {
            replacePre = "";
        }
        // then add a comma
        if (misMultivalued) {
            return replacePre + displ + DISPLAY_SEPARATOR;
        } else {
            return displ;
        }
    }

    public SuggestBox getSuggestBox() {
        return mfield;
    }

    @Override
    public int getTabIndex() {
        return mfield.getTabIndex();
    }

    /**
     * Get the value(s) as a String. If allowing multivalues, separated by the
     * VALUE_DELIM
     * 
     * @return value(s) as a String
     */
    public String getValue() {
        // String together all the values in the valueMap
        // based on the display values shown in the field
        final String text = mfield.getText();

        String values = "";
        String invalids = "";
        String newKeys = "";
        if (misMultivalued) {
            final String[] keys = text.split(DISPLAY_SEPARATOR);
            for (String key : keys) {
                key = key.trim();
                if (!key.isEmpty()) {
                    final String v = mvalueMap.get(key);
                    Log.info("getValue for key = " + key + " is v = " + v);
                    if (null != v) {
                        values += v + VALUE_DELIM;
                        // rebuild newKeys removing invalids and dups
                        newKeys += key + DISPLAY_SEPARATOR;
                    } else {
                        invalids += key + DISPLAY_SEPARATOR;
                    }
                }
            }
            values = trimLastDelimiter(values, VALUE_DELIM);
            // set the new display values
            mfield.setText(newKeys);
        } else {
            values = mvalueMap.get(text);
        }

        // if there were any invalid show warning
        if (!invalids.isEmpty()) {
            // trim last separator
            invalids = trimLastDelimiter(invalids, DISPLAY_SEPARATOR);
            updateFormFeedback(FormFeedback.ERROR, "Invalids: " + invalids);
        }
        return values;
    }

    /**
     * Get the value map
     * 
     * @return value map
     */
    public Map<String, String> getValueMap() {
        return mvalueMap;
    }

    @Override
    public void onKeyUp(final KeyUpEvent event) {
        /*
         * Because SuggestOracle.requestSuggestions does not get called when the
         * text field is empty this key up handler is necessary for handling the
         * case when there is an empty text field... Here, the FormFeedback is
         * reset.
         */
        updateFormFeedback(FormFeedback.NONE, null);
    }

    @Override
    public void onSelection(final SelectionEvent<Suggestion> event) {
        final Suggestion suggestion = event.getSelectedItem();
        if (suggestion instanceof OptionSuggestion) {
            final OptionSuggestion osugg = (OptionSuggestion) suggestion;
            // if NEXT or PREVIOUS were selected, requery but bypass the timer
            final String value = osugg.getValue();
            if (OptionSuggestion.NEXT_VALUE.equals(value)) {
                mindexFrom += PAGE_SIZE;
                mindexTo += PAGE_SIZE;

                final RestSuggestOracle oracle = (RestSuggestOracle) mfield.getSuggestOracle();
                oracle.getSuggestions();

            } else if (OptionSuggestion.PREVIOUS_VALUE.equals(value)) {
                mindexFrom -= PAGE_SIZE;
                mindexTo -= PAGE_SIZE;

                final RestSuggestOracle oracle = (RestSuggestOracle) mfield.getSuggestOracle();
                oracle.getSuggestions();

            } else {
                // made a valid selection
                updateFormFeedback(FormFeedback.VALID, null);

                // add the option's value to the value map
                putValue(osugg.getName(), value);

                // put the focus back into the textfield so user
                // can enter more
                // Commented mfield.setFocus(true);
            }
        }
    }

    private void putValue(final String key, final String value) {
        Log.info("putting key = " + key + "; value = " + value);
        mvalueMap.put(key, value);
    }

    /**
     * Retrieve Options (name-value pairs) that are suggested from the REST
     * endpoint
     * 
     * @param query
     *          - the String search term
     * @param from
     *          - the 0-based begin index int
     * @param to
     *          - the end index inclusive int
     * @param callback
     *          - the OptionQueryCallback to handle the response
     */
    private void queryOptions(final String query, final int from, final int to,
            final OptionQueryCallback callback) {
        final RequestBuilder builder = new RequestBuilder(RequestBuilder.GET,
                URL.encode(mrestEndpointUrl + "?" + SearcherConstants.QUERY_PARAM + "=" + query + "&"
                        + SearcherConstants.START_PARAM + "=" + from + "&" + SearcherConstants.LIMIT_PARAM + "="
                        + PAGE_SIZE));

        // Set our headers
        builder.setHeader("Accept", "application/json; charset=utf-8");

        // Fails on chrome
        // builder.setHeader("Accept-Charset", "UTF-8");

        builder.setCallback(new RequestCallback() {

            @Override
            public void onError(final com.google.gwt.http.client.Request request, final Throwable exception) {
                callback.error(exception);
            }

            @Override
            public void onResponseReceived(final com.google.gwt.http.client.Request request,
                    final Response response) {
                final JSONValue val = JSONParser.parse(response.getText());
                final JSONObject obj = val.isObject();
                final int totSize = (int) obj.get(OptionResultSet.TOTAL_SIZE).isNumber().doubleValue();
                final OptionResultSet options = new OptionResultSet(totSize);
                final JSONArray optionsArray = obj.get(OptionResultSet.OPTIONS).isArray();

                if (options.getTotalSize() > 0 && optionsArray != null) {

                    for (int i = 0; i < optionsArray.size(); i++) {
                        if (optionsArray.get(i) == null) {
                            /*
                             * This happens when a JSON array has an invalid trailing comma
                             */
                            continue;
                        }

                        final JSONObject jsonOpt = optionsArray.get(i).isObject();
                        final Option option = new Option();

                        final String longName = jsonOpt.get(OptionResultSet.DISPLAY_NAME).isString().stringValue();
                        final String shortName = jsonOpt.get(OptionResultSet.VALUE).isString().stringValue();
                        final JSONValue groupTypeJsonValue = jsonOpt.get("groupType");
                        final String prefix = groupTypeJsonValue.isString() == null ? ""
                                : GroupType.PERSONAL.name().equals(groupTypeJsonValue.isString().stringValue())
                                        ? I18n.t("User") + ": "
                                        : I18n.t("Group") + ": ";
                        option.setName(prefix
                                + (!longName.equals(shortName) ? longName + " (" + shortName + ")" : shortName));
                        option.setValue(jsonOpt.get(OptionResultSet.VALUE).isString().stringValue());
                        options.addOption(option);
                    }
                }
                callback.success(options);
            }
        });

        try {
            if (lastQuery != null && lastQuery.isPending()) {
                lastQuery.cancel();
            }
            lastQuery = builder.send();
        } catch (final RequestException e) {
            updateFormFeedback(FormFeedback.ERROR, "Error: " + e.getMessage());
        }

    }

    private void resetPageIndices() {
        mindexFrom = 0;
        mindexTo = mindexFrom + PAGE_SIZE - 1;
    }

    @Override
    public void setAccessKey(final char key) {
        mfield.setAccessKey(key);
    }

    @Override
    public void setFocus(final boolean focused) {
        mfield.setFocus(focused);
    }

    public void setSearchUrl(final String searchUrl) {
        mrestEndpointUrl = searchUrl;
    }

    @Override
    public void setTabIndex(final int index) {
        mfield.setTabIndex(index);
    }

    /**
     * Convenience method to set the status and tooltip of the FormFeedback
     * 
     * @param status
     *          - a FormFeedback status
     * @param tooltip
     *          - a String tooltip
     */
    public void updateFormFeedback(final int status, final String tooltip) {
        mfeedback.setStatus(status);
        if (tooltip != null) {
            mfeedback.setTitle(tooltip);
        }

        final TextBoxBase textBox = mfield.getTextBox();
        if (FormFeedback.LOADING == status) {
            NotifyUser.showProgressSearching();
            // textBox.setEnabled(false);
        } else {
            new Timer() {
                @Override
                public void run() {
                    NotifyUser.hideProgress();
                }
            }.schedule(1500);
            // textBox.setEnabled(true);
            textBox.setFocus(false); // Blur then focus b/c of a strange problem with
                                     // the cursor or selection highlights no longer
                                     // visible within the textfield (this is a
                                     // workaround)
            textBox.setFocus(true);
        }
    }
}