Package tsd.client

Source Code of tsd.client.RemoteOracle$QueriesSeen

// This file is part of OpenTSDB.
// Copyright (C) 2010-2012  The OpenTSDB Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 2.1 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 Lesser
// General Public License for more details.  You should have received a copy
// of the GNU Lesser General Public License along with this program.  If not,
// see <http://www.gnu.org/licenses/>.
package tsd.client;

import java.util.HashMap;

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.json.client.JSONArray;
import com.google.gwt.json.client.JSONParser;
import com.google.gwt.json.client.JSONValue;
import com.google.gwt.user.client.ui.HasText;
import com.google.gwt.user.client.ui.MultiWordSuggestOracle;
import com.google.gwt.user.client.ui.SuggestBox;
import com.google.gwt.user.client.ui.SuggestOracle;
import com.google.gwt.user.client.ui.TextBoxBase;

/**
* An oracle that gets suggestions through an AJAX call and provides caching.
*
* The oracle builds up a local cache of known suggestions and tries to avoid
* unnecessary requests when the cache can be used (which is fairly frequent
* given the typing pattern) or when we know for sure there won't be any
* results.
*
* The oracle is given a type.  Every instance that share the same type also
* share the same caches under the hood.  This is convenient when you want to
* have multiple text boxes with the same type of suggestions.
*/
final class RemoteOracle extends SuggestOracle {

  private static final String SUGGEST_URL = "/suggest?type="// + type&q=foo

  /**
   * Maps an oracle type to its suggestion cache.
   * The cache is in fact a {@link MultiWordSuggestOracle}, which we re-use as
   * its implementation is good (it uses a trie, handles HTML formatting etc.).
   */
  private static final HashMap<String, MultiWordSuggestOracle> caches =
    new HashMap<String, MultiWordSuggestOracle>();

  /** Maps an oracle type to the queries recently seen for this type. */
  private static final HashMap<String, QueriesSeen> all_queries_seen =
    new HashMap<String, QueriesSeen>();

  private final String type;
  private final MultiWordSuggestOracle cache;
  private final QueriesSeen queries_seen;

  /** Which widget are we wrapping to provide suggestions. */
  private HasText requester;
  /** Current ongoing request, or null. */
  private Callback current;

  /**
   * Pending request that arrived while we were still processing `current'.
   * If requests keep coming in while we're processing `current', the last
   * pending one will overwrite the previous pending one.
   */
  private Request pending_req;
  private Callback pending_cb;

  /** Used to guess whether we need to fetch more suggestions. */
  private String last_query;
  private String last_suggestion;

  /**
   * Factory method to use in order to get a {@link RemoteOracle} instance.
   * @param suggest_type The type of suggestion wanted.
   * @param textbox The text box to wrap to provide suggestions to.
   */
  public static SuggestBox newSuggestBox(final String suggest_type,
                                         final TextBoxBase textbox) {
    final RemoteOracle oracle = new RemoteOracle(suggest_type);
    final SuggestBox box = new SuggestBox(oracle, textbox);
    oracle.requester = box;
    return box;
  }

  /** Private constructor, use {@link #newSuggestBox} instead. */
  private RemoteOracle(final String suggest_type) {
    type = suggest_type;
    MultiWordSuggestOracle cache = caches.get(type);
    QueriesSeen queries_seen;
    if (cache == null) {
      cache = new MultiWordSuggestOracle(".");
      queries_seen = new QueriesSeen();
      caches.put(type, cache);
      all_queries_seen.put(type, queries_seen);
    } else {
      queries_seen = all_queries_seen.get(type);
    }
    this.cache = cache;
    this.queries_seen = queries_seen;
  }

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

  @Override
  public void requestSuggestions(final Request request, final Callback callback) {
    if (current != null) {
      pending_req = request;
      pending_cb = callback;
      return;
    }
    current = callback;
    {
      final String this_query = request.getQuery();
      // Check if we can serve this from our local cache, without even talking
      // to the server.  This is possible if either of those is true:
      //   1. We've already seen this query recently.
      //   2. This new query precedes another one and the user basically just
      //      typed another letter, so if the new query is "less than" the last
      //      result we got from the server, we know we already cached the full
      //      range of results covering the new request.
      if ((last_query != null
           && last_query.compareTo(this_query) <= 0
           && this_query.compareTo(last_suggestion) < 0)
          || queries_seen.check(this_query)) {
        current = null;
        cache.requestSuggestions(request, callback);
        return;
      }
      last_query = this_query;
    }

    final RequestBuilder builder = new RequestBuilder(RequestBuilder.GET,
      SUGGEST_URL + type + "&q=" + last_query);
    try {
      builder.sendRequest(null, new RequestCallback() {
        public void onError(final com.google.gwt.http.client.Request r,
                            final Throwable e) {
          current = null// Something bad happened, drop the current request.
          if (pending_req != null) {  // But if we have another waiting...
            requestSuggestions(pending_req, pending_cb)// ... try it now.
          }
        }

        // Need to use fully-qualified names as this class inherits already
        // from a pair of inner classes called Request / Response :-/
        public void onResponseReceived(final com.google.gwt.http.client.Request r,
                                       final com.google.gwt.http.client.Response response) {
          if (response.getStatusCode() == com.google.gwt.http.client.Response.SC_OK) {
            final JSONValue json = JSONParser.parse(response.getText());
            // In case this request returned nothing, we pretend the last
            // suggestion ended with the largest character possible, so we
            // won't send more requests to the server if the user keeps
            // adding extra characters.
            last_suggestion = last_query + "\377";
            if (json != null && json.isArray() != null) {
              final JSONArray results = json.isArray();
              final int n = Math.min(request.getLimit(), results.size());
              for (int i = 0; i < n; i++) {
                final JSONValue suggestion = results.get(i);
                if (suggestion == null || suggestion.isString() == null) {
                  continue;
                }
                final String suggestionstr = suggestion.isString().stringValue();
                last_suggestion = suggestionstr;
                cache.add(suggestionstr);
              }
              // Is this response still relevant to what the requester wants?
              if (requester.getText().startsWith(last_query)) {
                cache.requestSuggestions(request, callback);
                pending_req = null;
                pending_cb = null;
              }
            }
          }
          current = null// Regardless of what happened above, this is done.
          if (pending_req != null) {
            final Request req = pending_req;
            final Callback cb = pending_cb;
            pending_req = null;
            pending_cb = null;
            requestSuggestions(req, cb);
          }
        }
      });
    } catch (RequestException ignore) {
    }
  }

  /** Small circular buffer of queries already typed by the user. */
  private static final class QueriesSeen {

    /**
     * A circular buffer containing the last few requests already served.
     * It would be awesome if {@code gwt.user.client.ui.PrefixTree} wasn't
     * package-private, so we could use that instead.
     */
    private final String[] already_requested = new String[128];
    private int already_index;  // Index into already_index.

    /**
     * Checks whether or not we've already seen that query.
     */
    boolean check(final String query) {
      // Check most recent queries first, as they're the most likely to match
      // if the user goes back and forth by typing a few characters, removing
      // some, typing some more, etc.
      for (int i = already_index - 1; i >= 0; i--) {
        if (query.equals(already_requested[i])) {
          return true;
        }
      }
      for (int i = already_requested.length - 1; i >= already_index; i--) {
        if (query.equals(already_requested[i])) {
          return true;
        }
      }

      // First time we see this query, let's record it.
      already_requested[already_index++] = query;
      if (already_index == already_requested.length) {
        already_index = 0;
      }
      return false;
    }

  }

}
TOP

Related Classes of tsd.client.RemoteOracle$QueriesSeen

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.