tsd.client.RemoteOracle Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of opentsdb Show documentation
Show all versions of opentsdb Show documentation
OpenTSDB is a distributed, scalable Time Series Database (TSDB)
written on top of HBase. OpenTSDB was written to address a common need:
store, index and serve metrics collected from computer systems (network
gear, operating systems, applications) at a large scale, and make this
data easily accessible and graphable.
// 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 .
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 caches =
new HashMap();
/** Maps an oracle type to the queries recently seen for this type. */
private static final HashMap all_queries_seen =
new HashMap();
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;
}
}
}