diff --git a/org.eclipse.jgit.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.test/META-INF/MANIFEST.MF index 23ce1dafa..df4a39c88 100644 --- a/org.eclipse.jgit.test/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.test/META-INF/MANIFEST.MF @@ -41,6 +41,7 @@ Import-Package: com.googlecode.javaewah;version="[1.1.6,2.0.0)", org.eclipse.jgit.internal.storage.dfs;version="[6.5.0,6.6.0)", org.eclipse.jgit.internal.storage.file;version="[6.5.0,6.6.0)", org.eclipse.jgit.internal.storage.io;version="[6.5.0,6.6.0)", + org.eclipse.jgit.internal.storage.memory;version="[6.5.0,6.6.0)", org.eclipse.jgit.internal.storage.pack;version="[6.5.0,6.6.0)", org.eclipse.jgit.internal.storage.reftable;version="[6.5.0,6.6.0)", org.eclipse.jgit.internal.transport.connectivity;version="[6.5.0,6.6.0)", diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/memory/TernarySearchTreeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/memory/TernarySearchTreeTest.java new file mode 100644 index 000000000..623c92fc9 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/memory/TernarySearchTreeTest.java @@ -0,0 +1,330 @@ +/* + * Copyright (C) 2021, Matthias Sohn 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 static java.util.Map.entry; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import org.junit.Before; +import org.junit.Test; + +public class TernarySearchTreeTest { + + private TernarySearchTree tree; + + @Before + public void setup() { + tree = new TernarySearchTree<>(); + tree.insert("foo", "1"); + tree.insert("bar", "2"); + tree.insert("foobar", "3"); + tree.insert("foobaz", "4"); + tree.insert("johndoe", "5"); + } + + @Test + public void testInsert() { + int initialSize = tree.size(); + tree.insert("foobarbaz", "6"); + assertEquals(initialSize + 1, tree.size()); + assertEquals("6", tree.get("foobarbaz")); + } + + @Test + public void testBatchInsert() { + Map m = Map.ofEntries( + entry("refs/heads/master", "master"), + entry("refs/heads/stable-1.0", "stable-1.0"), + entry("refs/heads/stable-2.1", "stable-2.1"), + entry("refs/heads/stable-2.0", "stable-2.0"), + entry("refs/heads/stable-1.1", "stable-1.1"), + entry("refs/heads/stable-2.2", "stable-2.2"), + entry("aaa", "aaa"), entry("refs/tags/xyz", "xyz"), + entry("refs/tags/v1.1", "v1.1"), + entry("refs/tags/v2.2", "v2.2"), + entry("refs/tags/v1.0", "v1.0"), + entry("refs/tags/v2.1", "v2.1"), + entry("refs/tags/v2.0", "v2.0")); + tree.insert(m); + assertArrayEquals( + new String[] { "refs/heads/master", "refs/heads/stable-1.0", + "refs/heads/stable-1.1", "refs/heads/stable-2.0", + "refs/heads/stable-2.1", "refs/heads/stable-2.2" }, + toArray(tree.getKeysWithPrefix("refs/heads/"))); + assertArrayEquals( + new String[] { "refs/tags/v1.0", "refs/tags/v1.1", + "refs/tags/v2.0", "refs/tags/v2.1", "refs/tags/v2.2", + "refs/tags/xyz" }, + toArray(tree.getKeysWithPrefix("refs/tags/"))); + assertEquals("aaa", tree.get("aaa")); + } + + @Test + public void testInsertWithNullKey() { + Exception exception = assertThrows(IllegalArgumentException.class, + () -> { + tree.insert(null, "42"); + }); + assertTrue(exception.getMessage() + .contains("TernarySearchTree key must not be null or empty")); + } + + @Test + public void testOverwriteValue() { + int initialSize = tree.size(); + tree.insert("foo", "overwritten"); + assertEquals(initialSize, tree.size()); + assertEquals("overwritten", tree.get("foo")); + } + + @Test + public void testInsertNullValue() { + Exception exception = assertThrows(IllegalArgumentException.class, + () -> { + tree.insert("xxx", null); + }); + assertTrue(exception.getMessage() + .contains("cannot insert null value into TernarySearchTree")); + } + + @Test + public void testReloadAll() { + Map map = Map.of("foo", "foo-value", "bar", + "bar-value"); + tree.replace(map.entrySet()); + assertArrayEquals(new String[] { "bar", "foo" }, + toArray(tree.getKeys())); + } + + @Test + public void testReload() { + int initialSize = tree.size(); + Map map = Map.of("foo", "foo-value", "bar", + "bar-value"); + tree.reload(map.entrySet()); + assertEquals("foo-value", tree.get("foo")); + assertEquals("bar-value", tree.get("bar")); + assertEquals("3", tree.get("foobar")); + assertEquals("4", tree.get("foobaz")); + assertEquals("5", tree.get("johndoe")); + assertEquals(initialSize, tree.size()); + } + + @Test + public void testContains() { + assertTrue(tree.contains("foo")); + assertTrue(tree.contains("foobaz")); + assertFalse(tree.contains("ship")); + } + + @Test + public void testContainsWithNullKey() { + Exception exception = assertThrows(IllegalArgumentException.class, + () -> { + tree.contains(null); + }); + assertTrue(exception.getMessage() + .contains("TernarySearchTree key must not be null or empty")); + } + + @Test + public void testGet() { + assertEquals("1", tree.get("foo")); + assertEquals("5", tree.get("johndoe")); + assertNull(tree.get("ship")); + } + + @Test + public void testGetWithNullKey() { + Exception exception = assertThrows(IllegalArgumentException.class, + () -> { + tree.get(null); + }); + assertTrue(exception.getMessage() + .contains("TernarySearchTree key must not be null or empty")); + } + + @Test + public void testDeleteExisting() { + int initialSize = tree.size(); + tree.delete("foo"); + assertNull(tree.get("foo")); + assertEquals(initialSize - 1, tree.size()); + tree.delete("cake"); + assertEquals(4, tree.size()); + } + + @Test + public void testDeleteNonExisting() { + int initialSize = tree.size(); + tree.delete("non-existent-key"); + assertEquals(initialSize, tree.size()); + } + + @Test + public void testDeleteWithNullKey() { + Exception exception = assertThrows(IllegalArgumentException.class, + () -> { + tree.delete((String) null); + }); + assertTrue(exception.getMessage() + .contains("TernarySearchTree key must not be null or empty")); + } + + @Test + public void testDeleteMultiple() { + int initialSize = tree.size(); + List keys = toList(tree.getKeys()); + keys.remove("foobar"); + keys.remove("johndoe"); + tree.delete(Arrays.asList(new String[] { "foobar", "johndoe" })); + assertEquals(initialSize - 2, tree.size()); + assertArrayEquals(keys.toArray(), toArray(tree.getKeys())); + } + + @Test + public void testClear() { + assertEquals(5, tree.size()); + tree.clear(); + assertEquals(0, tree.size()); + tree.getKeys().forEach(new Consumer() { + + @Override + public void accept(String t) { + throw new IllegalStateException("should find no key"); + } + }); + } + + @Test + public void testKeyLongestPrefix() { + assertEquals("foobar", tree.keyLongestPrefixOf("foobari")); + assertEquals("foo", tree.keyLongestPrefixOf("foocake")); + assertEquals("", tree.keyLongestPrefixOf("faabar")); + assertEquals("johndoe", tree.keyLongestPrefixOf("johndoea")); + assertEquals("", tree.keyLongestPrefixOf("wxy")); + assertNull(tree.keyLongestPrefixOf("")); + assertNull(tree.keyLongestPrefixOf(null)); + } + + @Test + public void testGetKeys() { + assertArrayEquals( + new String[] { "bar", "foo", "foobar", "foobaz", "johndoe" }, + toArray(tree.getKeys())); + } + + @Test + public void testGetKeysWithPrefix() { + assertArrayEquals(new String[] { "foo", "foobar", "foobaz" }, + toArray(tree.getKeysWithPrefix("foo"))); + assertArrayEquals(new String[] { "johndoe" }, + toArray(tree.getKeysWithPrefix("john"))); + assertArrayEquals(new String[0], + toArray(tree.getKeysWithPrefix("cake"))); + assertArrayEquals( + new String[] { "bar", "foo", "foobar", "foobaz", "johndoe" }, + toArray(tree.getKeysWithPrefix(""))); + assertArrayEquals(new String[0], toArray(tree.getKeysWithPrefix(null))); + } + + @Test + public void testGetWithPrefixFoo() { + Map result = tree.getWithPrefix("foo"); + assertEquals(3, result.size()); + assertEquals("1", result.get("foo")); + assertEquals("3", result.get("foobar")); + assertEquals("4", result.get("foobaz")); + } + + @Test + public void testGetWithPrefixNotFound() { + Map result = tree.getWithPrefix("cheese"); + assertEquals(0, result.size()); + } + + @Test + public void testGetWithPrefixNull() { + Map result = tree.getWithPrefix(null); + assertEquals(0, result.size()); + } + + @Test + public void testGetWithPrefixEmptyPrefix() { + Map result = tree.getWithPrefix(""); + assertEquals(5, result.size()); + assertEquals("1", result.get("foo")); + assertEquals("2", result.get("bar")); + assertEquals("3", result.get("foobar")); + assertEquals("4", result.get("foobaz")); + assertEquals("5", result.get("johndoe")); + } + + @Test + public void testGetValuesWithPrefixFoo() { + List result = tree.getValuesWithPrefix("foo"); + assertEquals(3, result.size()); + assertArrayEquals(new String[] { "1", "3", "4" }, + result.toArray()); + } + + @Test + public void testGetValuesWithPrefixNotFound() { + List result = tree.getValuesWithPrefix("cheese"); + assertEquals(0, result.size()); + } + + @Test + public void testGetValuesWithPrefixNull() { + List result = tree.getValuesWithPrefix(null); + assertEquals(0, result.size()); + } + + @Test + public void testGetValuesWithPrefixEmptyPrefix() { + List result = tree.getValuesWithPrefix(""); + assertEquals(5, result.size()); + assertArrayEquals(new String[] { "2", "1", "3", "4", "5" }, + result.toArray()); + } + + @Test + public void testGetKeysMatching() { + assertArrayEquals(new String[] { "foobar" }, + toArray(tree.getKeysMatching("fo?bar"))); + assertArrayEquals(new String[] { "foobar", "foobaz" }, + toArray(tree.getKeysMatching("fooba?"))); + assertArrayEquals(new String[] { "foobar", "foobaz" }, + toArray(tree.getKeysMatching("?o?ba?"))); + assertArrayEquals(new String[0], toArray(tree.getKeysMatching(""))); + assertArrayEquals(new String[0], toArray(tree.getKeysMatching(null))); + } + + private static List toList(Iterable iter) { + return StreamSupport.stream(iter.spliterator(), false) + .collect(Collectors.toList()); + } + + private static String[] toArray(Iterable iter) { + return toList(iter).toArray(new String[0]); + } +} diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF index 453a716d8..1fad623a7 100644 --- a/org.eclipse.jgit/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit/META-INF/MANIFEST.MF @@ -101,6 +101,8 @@ Export-Package: org.eclipse.jgit.annotations;version="6.5.0", x-friends:="org.eclipse.jgit.junit, org.eclipse.jgit.test, org.eclipse.jgit.pgm", + org.eclipse.jgit.internal.storage.memory;version="6.5.0"; + x-friends:="org.eclipse.jgit.test", org.eclipse.jgit.internal.storage.pack;version="6.5.0"; x-friends:="org.eclipse.jgit.junit, org.eclipse.jgit.test, diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties index 836213286..bdf35d8d7 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -360,6 +360,8 @@ illegalArgumentNotA=Not {0} illegalCombinationOfArguments=The combination of arguments {0} and {1} is not allowed illegalHookName=Illegal hook name {0} illegalPackingPhase=Illegal packing phase {0} +illegalTernarySearchTreeKey=TernarySearchTree key must not be null or empty +illegalTernarySearchTreeValue=cannot insert null value into TernarySearchTree incorrectHashFor=Incorrect hash for {0}; computed {1} as a {2} from {3} bytes. incorrectOBJECT_ID_LENGTH=Incorrect OBJECT_ID_LENGTH. indexFileCorruptedNegativeBucketCount=Invalid negative bucket count read from pack v2 index file: {0} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java index f4f91f8aa..b502a1a98 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -388,6 +388,8 @@ public static JGitText get() { /***/ public String illegalCombinationOfArguments; /***/ public String illegalHookName; /***/ public String illegalPackingPhase; + /***/ public String illegalTernarySearchTreeKey; + /***/ public String illegalTernarySearchTreeValue; /***/ public String incorrectHashFor; /***/ public String incorrectOBJECT_ID_LENGTH; /***/ public String indexFileCorruptedNegativeBucketCount; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/memory/TernarySearchTree.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/memory/TernarySearchTree.java new file mode 100644 index 000000000..1ac662736 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/memory/TernarySearchTree.java @@ -0,0 +1,597 @@ +/* + * Copyright (C) 2021, Matthias Sohn 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 + * type of values in this tree + * @since 6.5 + */ +public final class TernarySearchTree { + + private static final char WILDCARD = '?'; + + private static class Node { + final char c; + + Node 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 + * type of values + */ + public static interface Loader { + /** + * Load map of all key value pairs + * + * @return map of all key value pairs to cache in the tree + */ + Map loadAll(); + } + + private static void validateKey(String key) { + if (StringUtils.isEmptyOrNull(key)) { + throw new IllegalArgumentException( + JGitText.get().illegalTernarySearchTreeKey); + } + } + + private static 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 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> loader) { + lock.writeLock().lock(); + try { + clear(); + for (Entry 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> loader) { + lock.writeLock().lock(); + try { + for (Entry 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 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 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 map) { + lock.writeLock().lock(); + try { + for (Entry 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 + * @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 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 getKeys() { + Queue 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 getKeysWithPrefix(String prefix) { + Queue keys = new LinkedList<>(); + if (prefix == null) { + return keys; + } + if (prefix.isEmpty()) { + return getKeys(); + } + lock.readLock().lock(); + try { + validateKey(prefix); + Node 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 getAll() { + Map 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 getAllValues() { + List 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 getWithPrefix(String prefix) { + Map entries = new HashMap<>(); + if (prefix == null) { + return entries; + } + if (prefix.isEmpty()) { + return getAll(); + } + lock.readLock().lock(); + try { + validateKey(prefix); + Node 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 getValuesWithPrefix(String prefix) { + List values = new ArrayList<>(); + if (prefix == null) { + return values; + } + if (prefix.isEmpty()) { + return getAllValues(); + } + lock.readLock().lock(); + try { + validateKey(prefix); + Node 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 getKeysMatching(String pattern) { + Queue keys = new LinkedList<>(); + lock.readLock().lock(); + try { + findKeysWithPrefix(root, new StringBuilder(), 0, pattern, keys); + return keys; + } finally { + lock.readLock().unlock(); + } + } + + private Node get(Node 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 insert(Node 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 node, StringBuilder prefix, + Queue 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 node, StringBuilder prefix, + Map 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 node, StringBuilder prefix, + List 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 node, StringBuilder prefix, + int i, String pattern, Queue 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); + } + } +}