//
//  ========================================================================
//  Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.util;

import java.nio.ByteBuffer;
import java.util.AbstractMap;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

A Ternary Trie String lookup data structure.

This Trie is of a fixed size and cannot grow (which can be a good thing with regards to DOS when used as a cache).

The Trie is stored in 3 arrays:

char[] _tree
This is semantically 2 dimensional array flattened into a 1 dimensional char array. The second dimension is that every 4 sequential elements represents a row of: character; hi index; eq index; low index, used to build a ternary trie of key strings.
String[] _key
An array of key values where each element matches a row in the _tree array. A non zero key element indicates that the _tree row is a complete key rather than an intermediate character of a longer key.
V[] _value
An array of values corresponding to the _key array

The lookup of a value will iterate through the _tree array matching characters. If the equal tree branch is followed, then the _key array is looked up to see if this is a complete match. If a match is found then the _value array is looked up to return the matching value.

This Trie may be instantiated either as case sensitive or insensitive.

This Trie is not Threadsafe and contains no mutual exclusion or deliberate memory barriers. It is intended for an ArrayTrie to be built by a single thread and then used concurrently by multiple threads and not mutated during that access. If concurrent mutations of the Trie is required external locks need to be applied.

Type parameters:
  • <V> – the Entry type
/** * <p>A Ternary Trie String lookup data structure.</p> * <p> * This Trie is of a fixed size and cannot grow (which can be a good thing with regards to DOS when used as a cache). * </p> * <p> * The Trie is stored in 3 arrays: * </p> * <dl> * <dt>char[] _tree</dt><dd>This is semantically 2 dimensional array flattened into a 1 dimensional char array. The second dimension * is that every 4 sequential elements represents a row of: character; hi index; eq index; low index, used to build a * ternary trie of key strings.</dd> * <dt>String[] _key</dt><dd>An array of key values where each element matches a row in the _tree array. A non zero key element * indicates that the _tree row is a complete key rather than an intermediate character of a longer key.</dd> * <dt>V[] _value</dt><dd>An array of values corresponding to the _key array</dd> * </dl> * <p>The lookup of a value will iterate through the _tree array matching characters. If the equal tree branch is followed, * then the _key array is looked up to see if this is a complete match. If a match is found then the _value array is looked up * to return the matching value. * </p> * <p> * This Trie may be instantiated either as case sensitive or insensitive. * </p> * <p>This Trie is not Threadsafe and contains no mutual exclusion * or deliberate memory barriers. It is intended for an ArrayTrie to be * built by a single thread and then used concurrently by multiple threads * and not mutated during that access. If concurrent mutations of the * Trie is required external locks need to be applied. * </p> * * @param <V> the Entry type */
public class ArrayTernaryTrie<V> extends AbstractTrie<V> { private static int LO = 1; private static int EQ = 2; private static int HI = 3;
The Size of a Trie row is the char, and the low, equal and high child pointers
/** * The Size of a Trie row is the char, and the low, equal and high * child pointers */
private static final int ROW_SIZE = 4;
The Trie rows in a single array which allows a lookup of row,character to the next row in the Trie. This is actually a 2 dimensional array that has been flattened to achieve locality of reference.
/** * The Trie rows in a single array which allows a lookup of row,character * to the next row in the Trie. This is actually a 2 dimensional * array that has been flattened to achieve locality of reference. */
private final char[] _tree;
The key (if any) for a Trie row. A row may be a leaf, a node or both in the Trie tree.
/** * The key (if any) for a Trie row. * A row may be a leaf, a node or both in the Trie tree. */
private final String[] _key;
The value (if any) for a Trie row. A row may be a leaf, a node or both in the Trie tree.
/** * The value (if any) for a Trie row. * A row may be a leaf, a node or both in the Trie tree. */
private final V[] _value;
The number of rows allocated
/** * The number of rows allocated */
private char _rows;
Create a case insensitive Trie of default capacity.
/** * Create a case insensitive Trie of default capacity. */
public ArrayTernaryTrie() { this(128); }
Create a Trie of default capacity
Params:
  • insensitive – true if the Trie is insensitive to the case of the key.
/** * Create a Trie of default capacity * * @param insensitive true if the Trie is insensitive to the case of the key. */
public ArrayTernaryTrie(boolean insensitive) { this(insensitive, 128); }
Create a case insensitive Trie
Params:
  • capacity – The capacity of the Trie, which is in the worst case is the total number of characters of all keys stored in the Trie. The capacity needed is dependent of the shared prefixes of the keys. For example, a capacity of 6 nodes is required to store keys "foo" and "bar", but a capacity of only 4 is required to store "bar" and "bat".
/** * Create a case insensitive Trie * * @param capacity The capacity of the Trie, which is in the worst case * is the total number of characters of all keys stored in the Trie. * The capacity needed is dependent of the shared prefixes of the keys. * For example, a capacity of 6 nodes is required to store keys "foo" * and "bar", but a capacity of only 4 is required to * store "bar" and "bat". */
public ArrayTernaryTrie(int capacity) { this(true, capacity); }
Create a Trie
Params:
  • insensitive – true if the Trie is insensitive to the case of the key.
  • capacity – The capacity of the Trie, which is in the worst case is the total number of characters of all keys stored in the Trie. The capacity needed is dependent of the shared prefixes of the keys. For example, a capacity of 6 nodes is required to store keys "foo" and "bar", but a capacity of only 4 is required to store "bar" and "bat".
/** * Create a Trie * * @param insensitive true if the Trie is insensitive to the case of the key. * @param capacity The capacity of the Trie, which is in the worst case * is the total number of characters of all keys stored in the Trie. * The capacity needed is dependent of the shared prefixes of the keys. * For example, a capacity of 6 nodes is required to store keys "foo" * and "bar", but a capacity of only 4 is required to * store "bar" and "bat". */
public ArrayTernaryTrie(boolean insensitive, int capacity) { super(insensitive); _value = (V[])new Object[capacity]; _tree = new char[capacity * ROW_SIZE]; _key = new String[capacity]; }
Copy Trie and change capacity by a factor
Params:
  • trie – the trie to copy from
  • factor – the factor to grow the capacity by
/** * Copy Trie and change capacity by a factor * * @param trie the trie to copy from * @param factor the factor to grow the capacity by */
public ArrayTernaryTrie(ArrayTernaryTrie<V> trie, double factor) { super(trie.isCaseInsensitive()); int capacity = (int)(trie._value.length * factor); _rows = trie._rows; _value = Arrays.copyOf(trie._value, capacity); _tree = Arrays.copyOf(trie._tree, capacity * ROW_SIZE); _key = Arrays.copyOf(trie._key, capacity); } @Override public void clear() { _rows = 0; Arrays.fill(_value, null); Arrays.fill(_tree, (char)0); Arrays.fill(_key, null); } @Override public boolean put(String s, V v) { int t = 0; int limit = s.length(); int last = 0; for (int k = 0; k < limit; k++) { char c = s.charAt(k); if (isCaseInsensitive() && c < 128) c = StringUtil.lowercases[c]; while (true) { int row = ROW_SIZE * t; // Do we need to create the new row? if (t == _rows) { _rows++; if (_rows >= _key.length) { _rows--; return false; } _tree[row] = c; } char n = _tree[row]; int diff = n - c; if (diff == 0) t = _tree[last = (row + EQ)]; else if (diff < 0) t = _tree[last = (row + LO)]; else t = _tree[last = (row + HI)]; // do we need a new row? if (t == 0) { t = _rows; _tree[last] = (char)t; } if (diff == 0) break; } } // Do we need to create the new row? if (t == _rows) { _rows++; if (_rows >= _key.length) { _rows--; return false; } } // Put the key and value _key[t] = v == null ? null : s; _value[t] = v; return true; } @Override public V get(String s, int offset, int len) { int t = 0; for (int i = 0; i < len; ) { char c = s.charAt(offset + i++); if (isCaseInsensitive() && c < 128) c = StringUtil.lowercases[c]; while (true) { int row = ROW_SIZE * t; char n = _tree[row]; int diff = n - c; if (diff == 0) { t = _tree[row + EQ]; if (t == 0) return null; break; } t = _tree[row + hilo(diff)]; if (t == 0) return null; } } return _value[t]; } @Override public V get(ByteBuffer b, int offset, int len) { int t = 0; offset += b.position(); for (int i = 0; i < len; ) { byte c = (byte)(b.get(offset + i++) & 0x7f); if (isCaseInsensitive()) c = (byte)StringUtil.lowercases[c]; while (true) { int row = ROW_SIZE * t; char n = _tree[row]; int diff = n - c; if (diff == 0) { t = _tree[row + EQ]; if (t == 0) return null; break; } t = _tree[row + hilo(diff)]; if (t == 0) return null; } } return (V)_value[t]; } @Override public V getBest(String s) { return getBest(0, s, 0, s.length()); } @Override public V getBest(String s, int offset, int length) { return getBest(0, s, offset, length); } private V getBest(int t, String s, int offset, int len) { int node = t; int end = offset + len; loop: while (offset < end) { char c = s.charAt(offset++); len--; if (isCaseInsensitive() && c < 128) c = StringUtil.lowercases[c]; while (true) { int row = ROW_SIZE * t; char n = _tree[row]; int diff = n - c; if (diff == 0) { t = _tree[row + EQ]; if (t == 0) break loop; // if this node is a match, recurse to remember if (_key[t] != null) { node = t; V better = getBest(t, s, offset, len); if (better != null) return better; } break; } t = _tree[row + hilo(diff)]; if (t == 0) break loop; } } return (V)_value[node]; } @Override public V getBest(ByteBuffer b, int offset, int len) { if (b.hasArray()) return getBest(0, b.array(), b.arrayOffset() + b.position() + offset, len); return getBest(0, b, offset, len); } private V getBest(int t, byte[] b, int offset, int len) { int node = t; int end = offset + len; loop: while (offset < end) { byte c = (byte)(b[offset++] & 0x7f); len--; if (isCaseInsensitive()) c = (byte)StringUtil.lowercases[c]; while (true) { int row = ROW_SIZE * t; char n = _tree[row]; int diff = n - c; if (diff == 0) { t = _tree[row + EQ]; if (t == 0) break loop; // if this node is a match, recurse to remember if (_key[t] != null) { node = t; V better = getBest(t, b, offset, len); if (better != null) return better; } break; } t = _tree[row + hilo(diff)]; if (t == 0) break loop; } } return (V)_value[node]; } private V getBest(int t, ByteBuffer b, int offset, int len) { int node = t; int o = offset + b.position(); loop: for (int i = 0; i < len; i++) { byte c = (byte)(b.get(o + i) & 0x7f); if (isCaseInsensitive()) c = (byte)StringUtil.lowercases[c]; while (true) { int row = ROW_SIZE * t; char n = _tree[row]; int diff = n - c; if (diff == 0) { t = _tree[row + EQ]; if (t == 0) break loop; // if this node is a match, recurse to remember if (_key[t] != null) { node = t; V best = getBest(t, b, offset + i + 1, len - i - 1); if (best != null) return best; } break; } t = _tree[row + hilo(diff)]; if (t == 0) break loop; } } return (V)_value[node]; } @Override public String toString() { StringBuilder buf = new StringBuilder(); for (int r = 0; r <= _rows; r++) { if (_key[r] != null && _value[r] != null) { buf.append(','); buf.append(_key[r]); buf.append('='); buf.append(_value[r].toString()); } } if (buf.length() == 0) return "{}"; buf.setCharAt(0, '{'); buf.append('}'); return buf.toString(); } @Override public Set<String> keySet() { Set<String> keys = new HashSet<>(); for (int r = 0; r <= _rows; r++) { if (_key[r] != null && _value[r] != null) keys.add(_key[r]); } return keys; } public int size() { int s = 0; for (int r = 0; r <= _rows; r++) { if (_key[r] != null && _value[r] != null) s++; } return s; } public boolean isEmpty() { for (int r = 0; r <= _rows; r++) { if (_key[r] != null && _value[r] != null) return false; } return true; } public Set<Map.Entry<String, V>> entrySet() { Set<Map.Entry<String, V>> entries = new HashSet<>(); for (int r = 0; r <= _rows; r++) { if (_key[r] != null && _value[r] != null) entries.add(new AbstractMap.SimpleEntry<>(_key[r], _value[r])); } return entries; } @Override public boolean isFull() { return _rows + 1 == _key.length; } public static int hilo(int diff) { // branchless equivalent to return ((diff<0)?LO:HI); // return 3+2*((diff&Integer.MIN_VALUE)>>Integer.SIZE-1); return 1 + (diff | Integer.MAX_VALUE) / (Integer.MAX_VALUE / 2); } public void dump() { for (int r = 0; r < _rows; r++) { char c = _tree[r * ROW_SIZE + 0]; System.err.printf("%4d [%s,%d,%d,%d] '%s':%s%n", r, (c < ' ' || c > 127) ? ("" + (int)c) : "'" + c + "'", (int)_tree[r * ROW_SIZE + LO], (int)_tree[r * ROW_SIZE + EQ], (int)_tree[r * ROW_SIZE + HI], _key[r], _value[r]); } } public static class Growing<V> implements Trie<V> { private final int _growby; private ArrayTernaryTrie<V> _trie; public Growing() { this(1024, 1024); } public Growing(int capacity, int growby) { _growby = growby; _trie = new ArrayTernaryTrie<>(capacity); } public Growing(boolean insensitive, int capacity, int growby) { _growby = growby; _trie = new ArrayTernaryTrie<>(insensitive, capacity); } @Override public int hashCode() { return _trie.hashCode(); } @Override public V remove(String s) { return _trie.remove(s); } @Override public boolean isCaseInsensitive() { return _trie.isCaseInsensitive(); } @Override public boolean equals(Object obj) { return _trie.equals(obj); } @Override public void clear() { _trie.clear(); } @Override public boolean put(V v) { return put(v.toString(), v); } @Override public boolean put(String s, V v) { boolean added = _trie.put(s, v); while (!added && _growby > 0) { ArrayTernaryTrie<V> bigger = new ArrayTernaryTrie<>(_trie._key.length + _growby); for (Map.Entry<String, V> entry : _trie.entrySet()) { bigger.put(entry.getKey(), entry.getValue()); } _trie = bigger; added = _trie.put(s, v); } return added; } @Override public V get(String s) { return _trie.get(s); } @Override public V get(ByteBuffer b) { return _trie.get(b); } @Override public V get(String s, int offset, int len) { return _trie.get(s, offset, len); } @Override public V get(ByteBuffer b, int offset, int len) { return _trie.get(b, offset, len); } @Override public V getBest(byte[] b, int offset, int len) { return _trie.getBest(b, offset, len); } @Override public V getBest(String s) { return _trie.getBest(s); } @Override public V getBest(String s, int offset, int length) { return _trie.getBest(s, offset, length); } @Override public V getBest(ByteBuffer b, int offset, int len) { return _trie.getBest(b, offset, len); } @Override public String toString() { return _trie.toString(); } @Override public Set<String> keySet() { return _trie.keySet(); } @Override public boolean isFull() { return false; } public void dump() { _trie.dump(); } public boolean isEmpty() { return _trie.isEmpty(); } public int size() { return _trie.size(); } } }