inet.ipaddr.format.util.AddressTrieMap Maven / Gradle / Ivy
/*
* Copyright 2020 Sean C Foley
*
* 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
* or at
* https://github.com/seancfoley/IPAddress/blob/master/LICENSE
*
* 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 inet.ipaddr.format.util;
import java.io.Serializable;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.Queue;
import java.util.Spliterator;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
import inet.ipaddr.Address;
import inet.ipaddr.format.util.AddressTrie.AddressBounds;
import inet.ipaddr.format.util.AddressTrieSet.Range;
import inet.ipaddr.format.util.AssociativeAddressTrie.AssociativeTrieNode;
/**
* Wraps a {@link inet.ipaddr.format.util.AssociativeAddressTrie} to view it as a Java Collections Framework map,
* implementing the {@link java.util.Map}, {@link java.util.SortedMap}, and {@link java.util.NavigableMap} interfaces.
*
* Like {@link java.util.TreeMap}, this map is backed by a binary tree and implements the same interfaces that {@link java.util.TreeMap} does.
* But there are some significant differences between the two binary tree implementations.
*
* A trie is naturally balanced and can only reach a depth corresponding to the number of bits in the keys,
* which is 32 for IPv4 and 128 for IPv6 tries. The TreeMap is balanced using red-black balancing.
*
* The {@link inet.ipaddr.format.util.AssociativeAddressTrie} allows you to modify the map entries using {@link java.util.Map.Entry#setValue(Object)},
* while {@link java.util.TreeMap} does not. The entries provided by the {@link java.util.TreeMap} are copies of the original nodes,
* so that the original nodes can be re-purposed. The nodes are not exposed.
*
* In the {@link inet.ipaddr.format.util.AssociativeAddressTrie} nodes are not re-purposed, and in fact they are also exposed.
* This enables navigation through the nodes.
* The node hierarchy has a special meaning, there is only one hierarchy for any given set of addresses,
* since it is determined by prefix block subnet containment. The hierarchy enables certain address-specific containment-based operations,
* such as subnet deletion or containment checks.
*
* In the trie map, when doing lookups and some other operations, only parts of the address keys are examined at each node in the binary tree search,
* rather than comparisons of the whole key, as with {@link java.util.TreeMap}.
* The trie map supports only the one comparison representing subnet containment, which is based on bit values and prefix length.
* The TreeMap is a general-purpose map supporting any natural ordering or Comparator.
*
* With the trie map, only addresses that are either individual address or prefix block subnets of the same type and version can be added to the trie,
* see {@link inet.ipaddr.format.util.AddressTrie.AddressComparator} for a comparator for the ordering.
*
* Should you wish to store, in a map, address instances that are not individual address or prefix block subnets,
* you can use {@link java.util.TreeMap} or any other Java collections framework map to store addresses of any type,
* or addresses of different versions or types in the same map,
* since all address items in this library are comparable with a natural ordering.
* There are additional orderings provided by this library as well, see {@link inet.ipaddr.AddressComparator}.
*
*
*
* @author scfoley
*
* @param the address type
* @param the type of the mapped values
*/
public class AddressTrieMap extends AbstractMap implements NavigableMap, Cloneable, Serializable {
private static final long serialVersionUID = 1L;
private AssociativeAddressTrie trie; // the backing trie
private final boolean isReverse;
private final Range bounds;
private EntrySet entrySet; // cached
private AddressTrieSet keySet; // cached
private AddressTrieMap descending; // cached
public AddressTrieMap(AssociativeAddressTrie trie) {
this.trie = trie;
this.isReverse = false;
this.bounds = null;
if(trie.map == null) {
trie.map = this;
}
}
public AddressTrieMap(AssociativeAddressTrie trie, Map extends K, ? extends V> map) {
this.trie = trie;
this.isReverse = false;
this.bounds = null;
if(trie.map == null) {
trie.map = this;
}
putAll(map);
}
AddressTrieMap(AssociativeAddressTrie trie, Range bounds, boolean isReverse) {
this.trie = trie;
this.bounds = bounds;
this.isReverse = isReverse;
if(trie.map == null && !isReverse && bounds == null) {
trie.map = this;
}
}
boolean isBounded() {
return bounds != null;
}
@Override
public AddressTrieMap descendingMap() {
AddressTrieMap desc = descending;
if(desc == null) {
Range reverseBounds = isBounded() ? bounds.reverse() : null;
desc = new AddressTrieMap(trie, reverseBounds, !isReverse);
descending = desc;
desc.descending = this;
}
return desc;
}
@Override
public AddressTrieSet descendingKeySet() {
return descendingMap().keySet();
}
/**
* Return a trie representing this map.
*
* If this map has a restricted range, see {@link #hasRestrictedRange()},
* this generates a new trie corresponding to the map with only the nodes pertaining to the restricted range sub-map.
* Otherwise this returns the original backing trie for this map.
*
* When a new trie is generated, the original backing trie for this map remains the same, it is not changed to the new trie.
*
* The returned trie will always have the same natural trie ordering,
* even if this map has the reverse ordering.
*
*/
public AssociativeAddressTrie asTrie() {
if(isBounded()) {
return trie.clone();
}
if(!isReverse) {
trie.map = this;// in case we constructed the set first, we put a reference back to us
}
return trie;
}
/**
* Returns whether this map is the result of a call to {@link #headMap(Address)}, {@link #tailMap(Address)},
* {@link #subMap(Address, Address)} or any of the other methods with the same names.
*
* @return
*/
public boolean hasRestrictedRange() {
return isBounded();
}
/**
* Returns the range if this map has a restricted range, see {@link #hasRestrictedRange()}. Otherwise returns null.
*
* @return
*/
public Range getRange() {
return bounds;
}
public static class EntrySet extends AbstractSet> implements Serializable {
private static final long serialVersionUID = 1L;
AssociativeAddressTrie trie;
private final boolean isReverse;
EntrySet(AssociativeAddressTrie trie, boolean isReverse) {
this.trie = trie;
this.isReverse = isReverse;
}
@SuppressWarnings("unchecked")
@Override
public Iterator> iterator() {
Iterator extends Entry> result = trie.nodeIterator(!isReverse);
return (Iterator>) result;
}
/**
* Returns an iterator that visits containing subnet blocks before their contained addresses and subnet blocks.
*
*/
@SuppressWarnings("unchecked")
public Iterator> containingFirstIterator() {
Iterator extends Entry> it = trie.containingFirstIterator(!isReverse);
return (Iterator>) it;
}
/**
* Returns an iterator that visits contained addresses and subnet blocks before their containing subnet blocks.
* @return
*/
@SuppressWarnings("unchecked")
public Iterator> containedFirstIterator() {
Iterator extends Entry> it = trie.containedFirstIterator(!isReverse);
return (Iterator>) it;
}
/**
* Iterates from largest prefix blocks to smallest to individual addresses.
*
* @return
*/
@SuppressWarnings("unchecked")
public Iterator> blockSizeIterator() {
Iterator extends Entry> iterator = trie.blockSizeNodeIterator(!isReverse);
return (Iterator>) iterator;
}
@SuppressWarnings("unchecked")
@Override
public Spliterator> spliterator() {
Spliterator extends Entry> result = trie.nodeSpliterator(!isReverse);
return (Spliterator>) result;
}
@Override
public int size() {
return trie.size();
}
@Override
public boolean isEmpty() {
return trie.isEmpty();
}
@SuppressWarnings("unchecked")
@Override
public boolean contains(Object o) {
if (!(o instanceof Entry)) {
return false;
}
Entry entry = (Entry) o;
Entry existingNode = trie.getAddedNode(entry.getKey());
return existingNode != null && Objects.equals(existingNode.getValue(), entry.getValue());
}
@SuppressWarnings("unchecked")
@Override
public boolean remove(Object o) {
if (!(o instanceof Entry)) {
return false;
}
Entry entry = (Entry) o;
AssociativeTrieNode existingNode = trie.getAddedNode(entry.getKey());
if(existingNode != null && Objects.equals(existingNode.getValue(), entry.getValue())) {
existingNode.remove();
return true;
}
return false;
}
@Override
public void clear() {
trie.clear();
}
@Override
public int hashCode() {
return trie.hashCode();
}
@Override
public boolean equals(Object o) {
if(o instanceof AddressTrieMap.EntrySet) {
EntrySet,?> other = (EntrySet,?>) o;
return trie.equals(other.trie);
}
return super.equals(o);
}
@Override
public boolean removeAll(Collection> collection) {
if(collection instanceof List || collection instanceof Queue || collection.size() < size()) {
boolean result = false;
for (Object object : collection) {
if(remove(object)) {
result = true;
}
}
return result;
}
return removeIf(collection::contains);
}
}
@Override
public AddressTrieSet keySet() {
AddressTrieSet set = keySet;
if(set == null) {
set = new AddressTrieSet(trie, bounds, isReverse);
keySet = set;
}
return set;
}
@Override
public AddressTrieSet navigableKeySet() {
return keySet();
}
@Override
public EntrySet entrySet() {
EntrySet set = entrySet;
if(set == null) {
set = new EntrySet(trie, isReverse);
entrySet = set;
}
return set;
}
@Override
public V merge(K key, V suppliedValue,
BiFunction super V, ? super V, ? extends V> remappingFunction) {
if(suppliedValue == null) {
throw new NullPointerException();
}
AssociativeTrieNode node = trie.remap(key, existingValue -> {
V newValue = (existingValue == null) ? suppliedValue : remappingFunction.apply(existingValue, suppliedValue);
return newValue;
});
if(node != null) {
return node.getValue();
}
return null;
}
@Override
public V compute(K key, BiFunction super K, ? super V, ? extends V> remappingFunction) {
AssociativeTrieNode node = trie.remap(key, existingValue -> {
V newValue = remappingFunction.apply(key, existingValue);
return newValue;
});
if(node != null) {
return node.getValue();
}
return null;
}
@Override
public V computeIfAbsent(K key, Function super K, ? extends V> remappingFunction) {
AssociativeTrieNode node = trie.remapIfAbsent(key, () -> remappingFunction.apply(key), false);
if(node != null) {
return node.getValue();
}
return null;
}
@Override
public V putIfAbsent(K key, V value) {
return trie.remapIfAbsent(key, () -> value, true).getValue();
}
@Override
public V computeIfPresent(K key, BiFunction super K, ? super V, ? extends V> remappingFunction) {
AssociativeTrieNode node = getNode(key);
if(node != null) {
V prevValue = node.getValue();
if(prevValue != null) {
V newValue = remappingFunction.apply(key, prevValue);
if (newValue != null) {
node.setValue(newValue);
} else {
node.remove();
}
return newValue;
}
}
return null;
}
@SuppressWarnings("unchecked")
@Override
public boolean containsKey(Object key) {
return trie.contains((K) key);
}
@Override
public boolean containsValue(Object value) {
Iterator extends AssociativeTrieNode> iterator = trie.nodeIterator(true);
while (iterator.hasNext()) {
AssociativeTrieNode node = iterator.next();
if (value.equals(node.getValue())) {
return true;
}
}
return false;
}
@SuppressWarnings("unchecked")
@Override
public V get(Object key) {
return trie.get((K) key);
}
/**
* Maps the given single address or prefix block subnet to the given value in the map.
*
* If the given address is not a single address nor prefix block, then this method throws IllegalArgumentException.
*
* See {@link AssociativeAddressTrie}
*/
@Override
public V put(K key, V value) {
return trie.put(key, value);
}
@SuppressWarnings("unchecked")
@Override
public V remove(Object key) {
AssociativeTrieNode node = getNode((K) key);
if(node != null) {
V result = node.getValue();
node.remove();
return result;
}
return null;
}
private AssociativeTrieNode getNode(K key) {
return (AssociativeTrieNode) trie.getAddedNode(key);
}
@SuppressWarnings("unchecked")
@Override
public V getOrDefault(Object key, V defaultValue) {
AssociativeTrieNode node = getNode((K) key);
return node == null ? defaultValue : node.getValue();
}
@Override
public void forEach(BiConsumer super K, ? super V> action) {
Iterator extends AssociativeTrieNode> iterator = trie.nodeIterator(!isReverse);
if(iterator.hasNext()) {
AssociativeTrieNode next = iterator.next();
action.accept(next.getKey(), next.getValue());
while(iterator.hasNext()) {
next = iterator.next();
action.accept(next.getKey(), next.getValue());
}
} else if(action == null) {
throw new NullPointerException();
}
}
@Override
public void replaceAll(BiFunction super K, ? super V, ? extends V> function) {
Iterator extends AssociativeTrieNode> iterator = trie.nodeIterator(!isReverse);
if(iterator.hasNext()) {
AssociativeTrieNode next = iterator.next();
next.setValue(function.apply(next.getKey(), next.getValue()));
while(iterator.hasNext()) {
next = iterator.next();
next.setValue(function.apply(next.getKey(), next.getValue()));
}
} else if(function == null) {
throw new NullPointerException();
}
}
@SuppressWarnings("unchecked")
@Override
public boolean remove(Object key, Object value) {
AssociativeTrieNode node = getNode((K) key);
if(node != null) {
V prevValue = node.getValue();
if(Objects.equals(value, prevValue)) {
node.remove();
return true;
}
}
return false;
}
@Override
public boolean replace(K key, V oldValue, V newValue) {
AssociativeTrieNode node = getNode(key);
if(node != null) {
V prevValue = node.getValue();
if(Objects.equals(oldValue, prevValue)) {
node.setValue(newValue);
return true;
}
}
return false;
}
@Override
public V replace(K key, V value) {
AssociativeTrieNode node = getNode(key);
if(node != null) {
V prevValue = node.getValue();
node.setValue(value);
return prevValue;
}
return null;
}
/**
* Returns the number of mappings in this map.
* This is a constant time operation, unless the map has a restricted range, see {@link #hasRestrictedRange()},
* in which case it is a linear time operation proportional to the number of mappings.
*
* @return the number of elements in this map
*/
@Override
public int size() {
return trie.size();
}
@Override
public boolean isEmpty() {
return trie.isEmpty();
}
@Override
public void clear() {
trie.clear();
}
@Override
public int hashCode() {
return trie.hashCode();
}
private AddressTrieMap toSubMap(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive) {
if(isReverse) {
K tmp = fromKey;
boolean tmpInc = fromInclusive;
fromKey = toKey;
fromInclusive = toInclusive;
toKey = tmp;
toInclusive = tmpInc;
}
AddressBounds bounds = trie.bounds, newBounds;
if(bounds == null) {
newBounds = AddressBounds.createNewBounds(fromKey, fromInclusive, toKey, toInclusive, trie.getComparator());
} else {
newBounds = bounds.restrict(fromKey, fromInclusive, toKey, toInclusive);
}
if(newBounds == null) {
return this;
}
Range newRange = new Range(newBounds, isReverse);
return new AddressTrieMap(trie.createSubTrie(newBounds), newRange, isReverse);
}
@Override
public AddressTrieMap subMap(K fromKey, K toKey) {
return subMap(fromKey, true, toKey, false);
}
@Override
public AddressTrieMap subMap(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive) {
if(fromKey == null || toKey == null) {
throw new NullPointerException();
}
return toSubMap(fromKey, fromInclusive, toKey, toInclusive);
}
@Override
public AddressTrieMap headMap(K toKey) {
return headMap(toKey, false);
}
@Override
public AddressTrieMap headMap(K toKey, boolean inclusive) {
if(toKey == null) {
throw new NullPointerException();
}
return toSubMap(null, true, toKey, inclusive);
}
@Override
public AddressTrieMap tailMap(K fromKey) {
return tailMap(fromKey, true);
}
@Override
public AddressTrieMap tailMap(K fromKey, boolean inclusive) {
if(fromKey == null) {
throw new NullPointerException();
}
return toSubMap(fromKey, inclusive, null, false);
}
@Override
public Entry firstEntry() {
AssociativeTrieNode node = isReverse ? trie.lastAddedNode() : trie.firstAddedNode();
if(node == null) {
return null;
}
return node;
}
@Override
public K firstKey() {
return keySet().first();
}
@Override
public Entry lastEntry() {
AssociativeTrieNode node = isReverse ? trie.firstAddedNode() : trie.lastAddedNode();
if(node == null) {
return null;
}
return node;
}
@Override
public K lastKey() {
return keySet().last();
}
@Override
public Entry lowerEntry(K key) {
AssociativeTrieNode node = isReverse ? trie.higherAddedNode(key) : trie.lowerAddedNode(key);
if(node == null) {
return null;
}
return node;
}
@Override
public K lowerKey(K key) {
return keySet().lower(key);
}
@Override
public Entry floorEntry(K key) {
AssociativeTrieNode node = isReverse ? trie.ceilingAddedNode(key) : trie.floorAddedNode(key);
if(node == null) {
return null;
}
return node;
}
@Override
public K floorKey(K key) {
return keySet().floor(key);
}
@Override
public Entry ceilingEntry(K key) {
AssociativeTrieNode node = isReverse ? trie.floorAddedNode(key) : trie.ceilingAddedNode(key);
if(node == null) {
return null;
}
return node;
}
@Override
public K ceilingKey(K key) {
return keySet().ceiling(key);
}
@Override
public Entry higherEntry(K key) {
AssociativeTrieNode node = isReverse ? trie.lowerAddedNode(key) : trie.higherAddedNode(key);
if(node == null) {
return null;
}
return node;
}
@Override
public K higherKey(K key) {
return keySet().higher(key);
}
@Override
public Entry pollFirstEntry() {
AssociativeTrieNode first = isReverse ? trie.lastAddedNode() : trie.firstAddedNode();
if(first == null) {
return null;
}
first.remove();
return first;
}
@Override
public Entry pollLastEntry() {
AssociativeTrieNode last = isReverse ? trie.firstAddedNode() : trie.lastAddedNode();
if(last == null) {
return null;
}
last.remove();
return last;
}
@Override
public boolean equals(Object o) {
if(o instanceof AddressTrieMap,?>) {
AddressTrieMap,?> other = (AddressTrieMap,?>) o;
// note that isReverse is ignored, intentionally
// two maps are equal if they have the same mappings
return trie.equals(other.trie);
}
return super.equals(o);
}
/**
* Clones the map along with the backing trie. If the map had a restricted range, the clone does not.
*/
@SuppressWarnings("unchecked")
@Override
public AddressTrieMap clone() {
try {
AddressTrieMap clone = (AddressTrieMap) super.clone();
clone.trie = trie.clone();
// cloning a trie eliminates the bounds, we we put them back
clone.trie.bounds = trie.bounds; //can share because bounds are immutable
clone.keySet = null;
clone.entrySet = null;
clone.descending = null;
return clone;
} catch (CloneNotSupportedException cannotHappen) {
return null;
}
}
@Override
public Comparator comparator() {
return isReverse ? AddressTrie.reverseComparator() : AddressTrie.comparator();
}
public String toTrieString() {
return trie.toString();
}
/**
* Returns a sub-map consisting of the mappings in the map with address keys contained by the given address
* The sub-map will have a restricted range matching the range of the given subnet or address.
*
* If the sub-map would be the same size as this map, then this map is returned.
* The sub-map will the same backing trie as this map.
*
* @param addr
* @return
*/
public AddressTrieMap subMapFromKeysContainedBy(K addr) {
AssociativeAddressTrie newTrie = trie.elementsContainedByToSubTrie(addr);
if(trie == newTrie) {
return this;
}
if(newTrie.bounds == null) {
return new AddressTrieMap(newTrie, null, isReverse);
}
Range newRange = new Range(newTrie.bounds, isReverse);
return new AddressTrieMap(newTrie, newRange, isReverse);
}
/**
* Returns a sub-map consisting of the mappings in the map with address keys that contain the given address.
* The sub-map will have the same restricted range (if any) as this sub-map.
*
* If the sub-map would be the same size as this map, then this map is returned.
* Otherwise, the sub-map is backed by a new trie.
* @param addr
* @return
*/
public AddressTrieMap subMapFromKeysContaining(K addr) {
AssociativeAddressTrie newTrie = trie.elementsContainingToTrie(addr);
if(trie == newTrie) {
return this;
}
if(newTrie.bounds == null) {
return new AddressTrieMap(newTrie, null, isReverse);
}
Range newRange = new Range(newTrie.bounds, isReverse);
return new AddressTrieMap(newTrie, newRange, isReverse);
}
/**
* Returns true if a subnet or address key in the map contains the given subnet or address.
*
* @param addr
* @return
*/
public boolean keyContains(K addr) {
return trie.elementContainsBounds(addr);
}
/**
* Returns the map entry corresponding to the key with the longest prefix match with the given address.
* @param addr
* @return
*/
public Entry longestPrefixMatchEntry(K addr) {
return trie.smallestElementContainingBounds(addr);
}
}