blob: acc1c830d586d0d8ebb42e6ffc69bb54b3abddef [file] [log] [blame]
/*
* Copyright (C) 2021, Matthias Sohn <matthias.sohn@sap.com> and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.internal.storage.memory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.eclipse.jgit.annotations.Nullable;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.util.StringUtils;
/**
* A ternary search tree with String keys and generic values.
*
* TernarySearchTree is a type of trie (sometimes called a prefix tree) where
* nodes are arranged in a manner similar to a binary search tree, but with up
* to three children rather than the binary tree's limit of two. Like other
* prefix trees, a ternary search tree can be used as an associative map
* structure with the ability for incremental string search. However, ternary
* search trees are more space efficient compared to standard prefix trees, at
* the cost of speed.
*
* Keys must not be null or empty. Values cannot be null.
*
* This class is thread safe.
*
* @param <Value>
* type of values in this tree
* @since 6.5
*/
public final class TernarySearchTree<Value> {
private static final char WILDCARD = '?';
private static class Node<Value> {
final char c;
Node<Value> lo, eq, hi;
Value val;
Node(char c) {
this.c = c;
}
boolean hasValue() {
return val != null;
}
}
/**
* Loader to load key-value pairs to be cached in the tree
*
* @param <Value>
* type of values
*/
public static interface Loader<Value> {
/**
* Load map of all key value pairs
*
* @return map of all key value pairs to cache in the tree
*/
Map<String, Value> loadAll();
}
private static void validateKey(String key) {
if (StringUtils.isEmptyOrNull(key)) {
throw new IllegalArgumentException(
JGitText.get().illegalTernarySearchTreeKey);
}
}
private static <V> void validateValue(V value) {
if (value == null) {
throw new IllegalArgumentException(
JGitText.get().illegalTernarySearchTreeValue);
}
}
private final ReadWriteLock lock;
private final AtomicInteger size = new AtomicInteger(0);
private Node<Value> root;
/**
* Construct a new ternary search tree
*/
public TernarySearchTree() {
lock = new ReentrantReadWriteLock();
}
/**
* Get the lock guarding read and write access to the cache.
*
* @return lock guarding read and write access to the cache
*/
public ReadWriteLock getLock() {
return lock;
}
/**
* Replace the tree with a new tree containing all entries provided by an
* iterable
*
* @param loader
* iterable providing key-value pairs to load
* @return number of key-value pairs after replacing finished
*/
public int replace(Iterable<Entry<String, Value>> loader) {
lock.writeLock().lock();
try {
clear();
for (Entry<String, Value> e : loader) {
insertImpl(e.getKey(), e.getValue());
}
} finally {
lock.writeLock().unlock();
}
return size.get();
}
/**
* Reload the tree entries provided by loader
*
* @param loader
* iterable providing key-value pairs to load
* @return number of key-value pairs
*/
public int reload(Iterable<Entry<String, Value>> loader) {
lock.writeLock().lock();
try {
for (Entry<String, Value> e : loader) {
insertImpl(e.getKey(), e.getValue());
}
} finally {
lock.writeLock().unlock();
}
return size.get();
}
/**
* Delete entries
*
* @param delete
* iterable providing keys of entries to be deleted
* @return number of key-value pairs
*/
public int delete(Iterable<String> delete) {
lock.writeLock().lock();
try {
for (String s : delete) {
delete(s);
}
} finally {
lock.writeLock().unlock();
}
return size.get();
}
/**
* Get the number of key value pairs in this trie
*
* @return number of key value pairs in this trie
*/
public int size() {
return size.get();
}
/**
* Get the value associated to a key or {@code null}.
*
* @param key
* the key
* @return the value associated to this key
*/
@Nullable
public Value get(String key) {
validateKey(key);
lock.readLock().lock();
try {
Node<Value> node = get(root, key, 0);
if (node == null) {
return null;
}
return node.val;
} finally {
lock.readLock().unlock();
}
}
/**
* Check whether this tree contains the given key.
*
* @param key
* a key
* @return whether this tree contains this key
*/
public boolean contains(String key) {
return get(key) != null;
}
/**
* Insert a key-value pair. If the key already exists the old value is
* overwritten.
*
* @param key
* the key
* @param value
* the value
* @return number of key-value pairs after adding the entry
*/
public int insert(String key, Value value) {
lock.writeLock().lock();
try {
insertImpl(key, value);
return size.get();
} finally {
lock.writeLock().unlock();
}
}
/**
* Insert map of key-value pairs. Values of existing keys are overwritten.
* Use this method to insert multiple key-value pairs.
*
* @param map
* map of key-value pairs to insert
* @return number of key-value pairs after adding entries
*/
public int insert(Map<String, Value> map) {
lock.writeLock().lock();
try {
for (Entry<String, Value> e : map.entrySet()) {
insertImpl(e.getKey(), e.getValue());
}
return size.get();
} finally {
lock.writeLock().unlock();
}
}
private void insertImpl(String key, Value value) {
validateValue(value);
if (!contains(key)) {
size.addAndGet(1);
}
root = insert(root, key, value, 0);
}
/**
* Delete a key-value pair. Does nothing if the key doesn't exist.
*
* @param key
* the key
* @return number of key-value pairs after the deletion
*/
public int delete(String key) {
lock.writeLock().lock();
try {
if (contains(key)) {
size.addAndGet(-1);
root = insert(root, key, null, 0);
}
return size.get();
} finally {
lock.writeLock().unlock();
}
}
/**
* Remove all key value pairs from this tree
*/
public void clear() {
lock.writeLock().lock();
try {
size.set(0);
root = null;
} finally {
lock.writeLock().unlock();
}
}
/**
* Find the key which is the longest prefix of the given query string.
*
* @param query
* the query string
* @return the key which is the longest prefix of the given query string or
* {@code null} if none exists.
*/
@Nullable
public String keyLongestPrefixOf(String query) {
if (StringUtils.isEmptyOrNull(query)) {
return null;
}
lock.readLock().lock();
try {
int length = 0;
Node<Value> node = root;
int i = 0;
while (node != null && i < query.length()) {
char c = query.charAt(i);
if (node.c > c) {
node = node.lo;
} else if (node.c < c) {
node = node.hi;
} else {
i++;
if (node.hasValue()) {
length = i;
}
node = node.eq;
}
}
return query.substring(0, length);
} finally {
lock.readLock().unlock();
}
}
/**
* Get all keys.
*
* @return all keys
*/
public Iterable<String> getKeys() {
Queue<String> queue = new LinkedList<>();
lock.readLock().lock();
try {
findKeysWithPrefix(root, new StringBuilder(), queue);
return queue;
} finally {
lock.readLock().unlock();
}
}
/**
* Get keys starting with given prefix
*
* @param prefix
* key prefix
* @return keys starting with given prefix
*/
public Iterable<String> getKeysWithPrefix(String prefix) {
Queue<String> keys = new LinkedList<>();
if (prefix == null) {
return keys;
}
if (prefix.isEmpty()) {
return getKeys();
}
lock.readLock().lock();
try {
validateKey(prefix);
Node<Value> node = get(root, prefix, 0);
if (node == null) {
return keys;
}
if (node.hasValue()) {
keys.add(prefix);
}
findKeysWithPrefix(node.eq, new StringBuilder(prefix), keys);
return keys;
} finally {
lock.readLock().unlock();
}
}
/**
* Get all entries.
*
* @return all entries
*/
public Map<String, Value> getAll() {
Map<String, Value> entries = new HashMap<>();
lock.readLock().lock();
try {
findWithPrefix(root, new StringBuilder(), entries);
return entries;
} finally {
lock.readLock().unlock();
}
}
/**
* Get all values.
*
* @return all values
*/
public List<Value> getAllValues() {
List<Value> values = new ArrayList<>();
lock.readLock().lock();
try {
findValuesWithPrefix(root, new StringBuilder(), values);
return values;
} finally {
lock.readLock().unlock();
}
}
/**
* Get all entries with given prefix
*
* @param prefix
* key prefix
* @return entries with given prefix
*/
public Map<String, Value> getWithPrefix(String prefix) {
Map<String, Value> entries = new HashMap<>();
if (prefix == null) {
return entries;
}
if (prefix.isEmpty()) {
return getAll();
}
lock.readLock().lock();
try {
validateKey(prefix);
Node<Value> node = get(root, prefix, 0);
if (node == null) {
return entries;
}
if (node.hasValue()) {
entries.put(prefix, node.val);
}
findWithPrefix(node.eq, new StringBuilder(prefix), entries);
return entries;
} finally {
lock.readLock().unlock();
}
}
/**
* Get all values with given key prefix
*
* @param prefix
* key prefix
* @return entries with given prefix
*/
public List<Value> getValuesWithPrefix(String prefix) {
List<Value> values = new ArrayList<>();
if (prefix == null) {
return values;
}
if (prefix.isEmpty()) {
return getAllValues();
}
lock.readLock().lock();
try {
validateKey(prefix);
Node<Value> node = get(root, prefix, 0);
if (node == null) {
return values;
}
if (node.hasValue()) {
values.add(node.val);
}
findValuesWithPrefix(node.eq, new StringBuilder(prefix), values);
return values;
} finally {
lock.readLock().unlock();
}
}
/**
* Get keys matching given pattern using '?' as wildcard character.
*
* @param pattern
* search pattern
* @return keys matching given pattern.
*/
public Iterable<String> getKeysMatching(String pattern) {
Queue<String> keys = new LinkedList<>();
lock.readLock().lock();
try {
findKeysWithPrefix(root, new StringBuilder(), 0, pattern, keys);
return keys;
} finally {
lock.readLock().unlock();
}
}
private Node<Value> get(Node<Value> node, String key, int depth) {
if (node == null) {
return null;
}
char c = key.charAt(depth);
if (node.c > c) {
return get(node.lo, key, depth);
} else if (node.c < c) {
return get(node.hi, key, depth);
} else if (depth < key.length() - 1) {
return get(node.eq, key, depth + 1);
} else {
return node;
}
}
private Node<Value> insert(Node<Value> node, String key, Value val,
int depth) {
char c = key.charAt(depth);
if (node == null) {
node = new Node<>(c);
}
if (node.c > c) {
node.lo = insert(node.lo, key, val, depth);
} else if (node.c < c) {
node.hi = insert(node.hi, key, val, depth);
} else if (depth < key.length() - 1) {
node.eq = insert(node.eq, key, val, depth + 1);
} else {
node.val = val;
}
return node;
}
private void findKeysWithPrefix(Node<Value> node, StringBuilder prefix,
Queue<String> keys) {
if (node == null) {
return;
}
findKeysWithPrefix(node.lo, prefix, keys);
if (node.hasValue()) {
keys.add(prefix.toString() + node.c);
}
findKeysWithPrefix(node.eq, prefix.append(node.c), keys);
prefix.deleteCharAt(prefix.length() - 1);
findKeysWithPrefix(node.hi, prefix, keys);
}
private void findWithPrefix(Node<Value> node, StringBuilder prefix,
Map<String, Value> entries) {
if (node == null) {
return;
}
findWithPrefix(node.lo, prefix, entries);
if (node.hasValue()) {
entries.put(prefix.toString() + node.c, node.val);
}
findWithPrefix(node.eq, prefix.append(node.c), entries);
prefix.deleteCharAt(prefix.length() - 1);
findWithPrefix(node.hi, prefix, entries);
}
private void findValuesWithPrefix(Node<Value> node, StringBuilder prefix,
List<Value> values) {
if (node == null) {
return;
}
findValuesWithPrefix(node.lo, prefix, values);
if (node.hasValue()) {
values.add(node.val);
}
findValuesWithPrefix(node.eq, prefix.append(node.c), values);
prefix.deleteCharAt(prefix.length() - 1);
findValuesWithPrefix(node.hi, prefix, values);
}
private void findKeysWithPrefix(Node<Value> node, StringBuilder prefix,
int i, String pattern, Queue<String> keys) {
if (node == null || StringUtils.isEmptyOrNull(pattern)) {
return;
}
char c = pattern.charAt(i);
if (c == WILDCARD || node.c > c) {
findKeysWithPrefix(node.lo, prefix, i, pattern, keys);
}
if (c == WILDCARD || node.c == c) {
if (i == pattern.length() - 1 && node.hasValue()) {
keys.add(prefix.toString() + node.c);
}
if (i < pattern.length() - 1) {
findKeysWithPrefix(node.eq, prefix.append(node.c), i + 1,
pattern, keys);
prefix.deleteCharAt(prefix.length() - 1);
}
}
if (c == WILDCARD || node.c < c) {
findKeysWithPrefix(node.hi, prefix, i, pattern, keys);
}
}
}