blob: 55d97f12aed800e2ec079cec21211a1d0a72ec4f [file] [log] [blame]
/*
* Copyright 2007 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.gwt.user.client.ui;
import com.google.gwt.core.client.JavaScriptObject;
import java.util.AbstractCollection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
/**
* A prefix tree (aka trie).
*
*/
class PrefixTree extends AbstractCollection<String> {
/**
* Iterates over the structure of a PrefixTree. No concurrency checks are
* made. This Iterator will output anything new added to the tree if the new
* entry is after the current position of the Iterator.
*
* This Iterator implementation is iterative and uses an internal stack object
* to avoid call-stack limitations and invocation overhead.
*/
private static class PrefixTreeIterator implements Iterator<String> {
// Called from JSNI.
private JavaScriptObject stack;
/**
* Constructor.
*
* @param tree The base of the PrefixTree to iterate over
*/
public PrefixTreeIterator(PrefixTree tree) {
init();
addTree(tree, "");
}
public boolean hasNext() {
// Have nextImpl peek at the next value that would be returned.
return nextImpl(true) != null;
}
/**
* {@inheritDoc} Wraps the native implementation with some sanity checking.
*/
public String next() {
final String toReturn = nextImpl(false);
// A null response indicates that there was no data to be had.
if (toReturn == null) {
// Sanity check.
if (!hasNext()) {
throw new NoSuchElementException("No more elements in the iterator");
} else {
throw new RuntimeException(
"nextImpl() returned null, but hasNext says otherwise");
}
}
return toReturn;
}
public void remove() {
throw new UnsupportedOperationException("PrefixTree does not support "
+ "removal. Use clear()");
}
/**
* Add a frame to the work stack.
*
* <pre>
* frame := {suffixNames, subtrees, prefix, index}
* suffixNames := All suffixes in the target PrefixTree
* subtrees := All subtrees in the target PrefixTree
* prefix := A string that next() will prepend to output from the frame
* index := Stores which suffix was last output
* </pre>
*
* @param tree The tree to add
* @param prefix The prefix to prepend to values in tree
*/
private native void addTree(PrefixTree tree, String prefix) /*-{
var suffixes = [];
for (var suffix in tree.@com.google.gwt.user.client.ui.PrefixTree::suffixes) {
// Ignore object properties that aren't colon-prepended keys
if (suffix.indexOf(':') == 0) {
suffixes.push(suffix);
}
}
var frame = {
suffixNames: suffixes,
subtrees: tree.@com.google.gwt.user.client.ui.PrefixTree::subtrees,
prefix: prefix,
index: 0
};
var stack = this.@com.google.gwt.user.client.ui.PrefixTree$PrefixTreeIterator::stack;
stack.push(frame);
}-*/;
/**
* Initialize JSNI objects.
*/
private native void init() /*-{
this.@com.google.gwt.user.client.ui.PrefixTree$PrefixTreeIterator::stack = [];
}-*/;
/**
* Access JSNI structures.
*
* @param peek If this is true, don't advance the iteration, just return the
* value that next() would return if it were called
* @return The next object, or null if there is an error
*/
private native String nextImpl(boolean peek) /*-{
var stack = this.@com.google.gwt.user.client.ui.PrefixTree$PrefixTreeIterator::stack;
var safe = @com.google.gwt.user.client.ui.PrefixTree::safe(Ljava/lang/String;)
var unsafe = @com.google.gwt.user.client.ui.PrefixTree::unsafe(Ljava/lang/String;)
// Put this in a while loop to handle descent into subtrees without recursion.
while (stack.length > 0) {
var frame = stack.pop();
// Check to see if there are any remaining suffixes to output.
if (frame.index < frame.suffixNames.length) {
var toReturn = frame.prefix + unsafe(frame.suffixNames[frame.index]);
if (!peek) {
frame.index++;
}
// If the current frame has more suffixes, retain it on the stack.
if (frame.index < frame.suffixNames.length) {
stack.push(frame);
// Otherwise, put all of the subtrees on the stack.
} else {
for (key in frame.subtrees) {
if (key.indexOf(':') != 0) {
continue;
}
var target = frame.prefix + unsafe(key);
var subtree = frame.subtrees[key];
this.@com.google.gwt.user.client.ui.PrefixTree$PrefixTreeIterator::addTree(Lcom/google/gwt/user/client/ui/PrefixTree;Ljava/lang/String;)(subtree, target);
}
}
return toReturn;
// Put all subframes on the stack, and return to top of loop.
} else {
for (var key in frame.subtrees) {
if (key.indexOf(':') != 0) {
continue;
}
var target = frame.prefix + unsafe(key);
var subtree = frame.subtrees[key];
this.@com.google.gwt.user.client.ui.PrefixTree$PrefixTreeIterator::addTree(Lcom/google/gwt/user/client/ui/PrefixTree;Ljava/lang/String;)(subtree, target);
}
}
}
// This would indicate that next() was called on an empty iterator.
// Will throw an exception from next().
return null;
}-*/;
}
/**
* Used by native methods to create an appropriately blessed PrefixTree.
*
* @param prefixLength Smaller prefix length equals faster, more direct
* searches, at a cost of setup time
* @return a newly constructed prefix tree
*/
protected static PrefixTree createPrefixTree(int prefixLength) {
return new PrefixTree(prefixLength);
}
/**
* Ensure that a String can be safely used as a key to an associative-array
* JavaScript object by prepending a prefix.
*
* @param s The String to make safe
* @return A safe version of <code>s</code>
*/
private static String safe(String s) {
return ':' + s;
}
/**
* Undo the operation performed by safe().
*
* @param s A String returned from safe()
* @return The original String passed into safe()
*/
private static String unsafe(String s) {
return s.substring(1);
}
/**
* Stores the requested prefix length.
*/
protected final int prefixLength;
/**
* Field to store terminal nodes in.
*/
protected JavaScriptObject suffixes;
/**
* Field to store subtrees in.
*/
protected JavaScriptObject subtrees;
/**
* Store the number of elements contained by this PrefixTree and its
* sub-trees.
*/
protected int size = 0;
/**
* Constructor.
*/
public PrefixTree() {
this(2, null);
}
/**
* Constructor.
*
* @param source Initialize from another collection
*/
public PrefixTree(Collection<String> source) {
this(2, source);
}
/**
* Constructor.
*
* @param prefixLength Smaller prefix length equals faster, more direct
* searches, at a cost of setup time.
*/
public PrefixTree(final int prefixLength) {
this(prefixLength, null);
}
/**
* Constructor.
*
* @param prefixLength Smaller prefix length equals faster, more direct
* searches, at a cost of setup time.
* @param source Initialize from another collection
*/
public PrefixTree(final int prefixLength, final Collection<String> source) {
this.prefixLength = prefixLength;
clear();
if (source != null) {
addAll(source);
}
}
/**
* Add a String to the PrefixTree.
*
* @param s The data to add
* @return <code>true</code> if the string was added, <code>false</code>
* otherwise
*/
@Override
public native boolean add(String s) /*-{
var suffixes =
this.@com.google.gwt.user.client.ui.PrefixTree::suffixes;
var subtrees =
this.@com.google.gwt.user.client.ui.PrefixTree::subtrees;
var prefixLength =
this.@com.google.gwt.user.client.ui.PrefixTree::prefixLength;
// This would indicate a mis-use of the code.
if ((s == null) || (s.length == 0)) {
return false;
}
// Use <= so that strings that are exactly prefixLength long don't
// require some kind of null token.
if (s.length <= prefixLength) {
var safeKey = @com.google.gwt.user.client.ui.PrefixTree::safe(Ljava/lang/String;)(s);
if (suffixes.hasOwnProperty(safeKey)) {
return false;
} else {
// Each tree keeps a count of how large it and its children are.
this.@com.google.gwt.user.client.ui.PrefixTree::size++;
suffixes[safeKey] = true;
return true;
}
// Add the string to the appropriate PrefixTree.
} else {
var prefix = @com.google.gwt.user.client.ui.PrefixTree::safe(Ljava/lang/String;)(s.slice(0, prefixLength));
var theTree;
if (subtrees.hasOwnProperty(prefix)) {
theTree = subtrees[prefix];
} else {
theTree = @com.google.gwt.user.client.ui.PrefixTree::createPrefixTree(I)(prefixLength << 1);
subtrees[prefix] = theTree;
}
var slice = s.slice(prefixLength);
if (theTree.@com.google.gwt.user.client.ui.PrefixTree::add(Ljava/lang/String;)(slice)) {
// The size of the subtree increased, so we need to update the local count.
this.@com.google.gwt.user.client.ui.PrefixTree::size++;
return true;
} else {
return false;
}
}
}-*/;
/**
* Initialize native state.
*/
@Override
public native void clear() /*-{
this.@com.google.gwt.user.client.ui.PrefixTree::size = 0;
this.@com.google.gwt.user.client.ui.PrefixTree::subtrees = {};
this.@com.google.gwt.user.client.ui.PrefixTree::suffixes = {};
}-*/;
@Override
public boolean contains(Object o) {
if (o instanceof String) {
return contains((String) o);
} else {
return false;
}
}
public boolean contains(String s) {
return (getSuggestions(s, 1)).contains(s);
}
/**
* Retrieve suggestions from the PrefixTree. The number of items returned from
* getSuggestions may slightly exceed <code>limit</code> so that all
* suffixes and partial stems will be returned. This prevents the search space
* from changing size if the PrefixTree is used in an interactive manner.
* <br/> The returned List is guaranteed to be safe; changing its contents
* will not affect the PrefixTree.
*
* @param search The prefix to search for
* @param limit The desired number of results to retrieve
* @return A List of suggestions
*/
public List<String> getSuggestions(String search, int limit) {
final List<String> toReturn = new ArrayList<String>();
if ((search != null) && (limit > 0)) {
suggestImpl(search, "", toReturn, limit);
}
return toReturn;
}
@Override
public Iterator<String> iterator() {
return new PrefixTreeIterator(this);
}
/**
* Get the number of all elements contained within the PrefixTree.
*
* @return the size of the prefix tree
*/
@Override
public int size() {
return size;
}
protected native void suggestImpl(String search, String prefix,
Collection<String> output, int limit) /*-{
var suffixes =
this.@com.google.gwt.user.client.ui.PrefixTree::suffixes;
var subtrees =
this.@com.google.gwt.user.client.ui.PrefixTree::subtrees;
var prefixLength =
this.@com.google.gwt.user.client.ui.PrefixTree::prefixLength;
// Search is too big to be found in current tree, just recurse.
if (search.length > prefix.length + prefixLength) {
var key = @com.google.gwt.user.client.ui.PrefixTree::safe(Ljava/lang/String;)(search.slice(prefix.length, prefix.length + prefixLength));
// Just pick the correct subtree, if it exists, and call suggestImpl.
if (subtrees.hasOwnProperty(key)) {
var subtree = subtrees[key];
var target = prefix + @com.google.gwt.user.client.ui.PrefixTree::unsafe(Ljava/lang/String;)(key);
subtree.@com.google.gwt.user.client.ui.PrefixTree::suggestImpl(Ljava/lang/String;Ljava/lang/String;Ljava/util/Collection;I)(search, target, output, limit);
}
// The answer can only exist in this tree's suffixes or subtree keys.
} else {
// Check local suffixes.
for (var suffix in suffixes) {
if (suffix.indexOf(':') != 0) {
continue;
}
var target = prefix + @com.google.gwt.user.client.ui.PrefixTree::unsafe(Ljava/lang/String;)(suffix);
if (target.indexOf(search) == 0) {
output.@java.util.Collection::add(Ljava/lang/Object;)(target);
}
if (output.@java.util.Collection::size()() >= limit) {
return;
}
}
// Check the subtree keys. If the key matches, that implies that all
// elements of the subtree would match.
for (var key in subtrees) {
if (key.indexOf(':') != 0) {
continue;
}
var target = prefix + @com.google.gwt.user.client.ui.PrefixTree::unsafe(Ljava/lang/String;)(key);
var subtree = subtrees[key];
// See if the prefix gives us a match.
if (target.indexOf(search) == 0) {
// Provide as many suggestions as will fit into the remaining limit.
// If there is only one suggestion, include it.
if ((subtree.@com.google.gwt.user.client.ui.PrefixTree::size <= limit - output.@java.util.Collection::size()()) ||
(subtree.@com.google.gwt.user.client.ui.PrefixTree::size == 1)) {
subtree.@com.google.gwt.user.client.ui.PrefixTree::dump(Ljava/util/Collection;Ljava/lang/String;)(output, target);
// Otherwise, include as many answers as we can by truncating the suffix
} else {
// Always fully-specify suffixes.
for (var suffix in subtree.@com.google.gwt.user.client.ui.PrefixTree::suffixes) {
if (suffix.indexOf(':') == 0) {
output.@java.util.Collection::add(Ljava/lang/Object;)(target + @com.google.gwt.user.client.ui.PrefixTree::unsafe(Ljava/lang/String;)(suffix));
}
}
// Give the keys of the subtree.
for (var subkey in subtree.@com.google.gwt.user.client.ui.PrefixTree::subtrees) {
if (subkey.indexOf(':') == 0) {
output.@java.util.Collection::add(Ljava/lang/Object;)(target + @com.google.gwt.user.client.ui.PrefixTree::unsafe(Ljava/lang/String;)(subkey) + "...");
}
}
}
}
}
}
}-*/;
/**
* Put all contents of the PrefixTree into a Collection.
*
* @param output the collection into which the prefixes will be dumped
* @param prefix the prefix to filter with
*/
private void dump(Collection<String> output, String prefix) {
for (String s : this) {
output.add(prefix + s);
}
}
}