// Copyright 2012 Google Inc. All Rights Reserved.
//
// 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.collide.client.code.autocomplete;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.shared.util.JsonCollections;
import com.google.common.base.Preconditions;
/**
* Trie-based implementation of the prefix index.
*
* @param <T> value object type
*/
public class AbstractTrie<T> implements PrefixIndex<T> {
// TODO: This member should be static and unmodifiable.
private final JsonArray<T> emptyList = JsonCollections.createArray();
private TrieNode<T> root;
public AbstractTrie() {
clear();
}
public TrieNode<T> getRoot() {
return root;
}
public void clear() {
this.root = TrieNode.<T>makeNode("");
}
/**
* @return {@code true} is this trie is empty
*/
public boolean isEmpty() {
return this.root.getChildren().isEmpty();
}
public TrieNode<T> put(String key, T value) {
return insertIntoTrie(key, root, value);
}
@Override
public JsonArray<T> search(String prefix) {
TrieNode<T> searchRoot = findNode(prefix, root);
return (searchRoot == null) ? emptyList : collectSubtree(searchRoot);
}
/**
* Returns all leaf nodes from a subtree rooted by {@code searchRoot} node
* and restricted with a {@code stopFunction}
*
* @param searchRoot node to start from
* @return all leaf nodes in the matching subtree
*/
public static <T> JsonArray<T> collectSubtree(TrieNode<T> searchRoot) {
JsonArray<TrieNode<T>> leaves = JsonCollections.createArray();
getAllLeavesInSubtree(searchRoot, leaves);
JsonArray<T> result = JsonCollections.createArray();
for (int i = 0; i < leaves.size(); i++) {
result.add(leaves.get(i).getValue());
}
return result;
}
/**
* Traverses the subtree rooted at {@code root} and collects all nodes in the
* subtree that are leaves (i.e., valid elements, not only prefixes).
*
* @param root a node in the trie from which to start collecting
* @param leaves output array
*/
private static <T> void getAllLeavesInSubtree(TrieNode<T> root, JsonArray<TrieNode<T>> leaves) {
if (root.getIsLeaf()) {
leaves.add(root);
}
for (int i = 0, size = root.getChildren().size(); i < size; i++) {
TrieNode<T> child = root.getChildren().get(i);
getAllLeavesInSubtree(child, leaves);
}
}
private TrieNode<T> insertIntoTrie(String prefix, TrieNode<T> node, T value) {
String nodePrefix = node.getPrefix();
if (nodePrefix.equals(prefix)) {
node.setValue(value);
return node;
} else {
TrieNode<T> branch = node.findInsertionBranch(prefix);
if (branch != null) {
return insertIntoTrie(prefix, branch, value);
} else {
// create new trie nodes
JsonArray<TrieNode<T>> suffixChain =
makeSuffixChain(
node, prefix.substring(nodePrefix.length()), value);
return suffixChain.peek();
}
}
}
/**
* Inserts a chain of children into the given node.
*
* @param root node to insert into
* @param suffix suffix of the last node in the chain
* @param value value of the last node in the chain
* @return the inserted chain in direct order (from the root to the leaf)
*/
JsonArray<TrieNode<T>> makeSuffixChain(TrieNode<T> root, String suffix, T value) {
JsonArray<TrieNode<T>> result = JsonCollections.createArray();
String rootPrefix = root.getPrefix();
for (int i = 1, suffixSize = suffix.length(); i <= suffixSize; i++) {
String newPrefix = rootPrefix + suffix.substring(0, i);
TrieNode<T> newNode = TrieNode.makeNode(newPrefix);
result.add(newNode);
root.addChild(newNode);
root = newNode;
}
root.setValue(value);
return result;
}
/**
* Searches the subtree rooted at {@code searchRoot} for a node
* corresponding to the prefix.
*
* There can only ever be one such node, or zero. If no node is found, returns
* null.
*
* Note that the {@code prefix} is relative to the whole trie root, not
* to the {@code searchRoot}.
*
* @param prefix the prefix to be found
* @param searchRoot the root of the subtree that is searched
* @return the node in the tree corresponding to prefix, or null if no such
* node exists
*/
public static <T> TrieNode<T> findNode(String prefix, TrieNode<T> searchRoot) {
Preconditions.checkNotNull(prefix);
if (prefix.equals(searchRoot.getPrefix())) {
return searchRoot;
}
TrieNode<T> closestAncestor = findClosestAncestor(prefix, searchRoot);
return (closestAncestor.getPrefix().equals(prefix)) ? closestAncestor : null;
}
/**
* Finds the closest ancestor of a search key. Formally it returns
* a node x, such that: {@code prefix.startsWith(x.prefix)}
* and there is no other node y, such that
* {@code (prefix.startsWith(y.prefix) and y.prefix.length > x.prefix)}
*
* @param key search key
* @param searchRoot node to start from
* @return closest ancestor
*/
static <T> TrieNode<T> findClosestAncestor(String key, TrieNode<T> searchRoot) {
Preconditions.checkNotNull(key);
Preconditions.checkArgument(key.startsWith(searchRoot.getPrefix()), "key=%s root prefix=%s",
key, searchRoot.getPrefix());
TrieNode<T> result = searchRoot;
for (TrieNode<T> child = searchRoot; child != null; child = child.findInsertionBranch(key)) {
result = child;
}
return result;
}
}