io.permazen.util.AbstractNavigableMap Maven / Gradle / Ivy
Show all versions of permazen-util Show documentation
/*
* Copyright (C) 2015 Archie L. Cobbs. All rights reserved.
*/
package io.permazen.util;
import com.google.common.base.Preconditions;
import java.util.AbstractMap;
import java.util.Comparator;
import java.util.Map;
import java.util.NavigableMap;
import java.util.NavigableSet;
import java.util.NoSuchElementException;
/**
* Support superclass for {@link NavigableMap} implementations based on database entries.
*
*
* For a read-only implementation, subclasses should implement {@link #comparator comparator()}, {@link #get get()},
* {@link #entrySet entrySet()}, {@link #navigableKeySet navigableKeySet()}, and {@link #createSubMap createSubMap()}
* to handle reversed and restricted range sub-maps.
*
*
* For a mutable implementation, subclasses should also implement {@link #put put()}, {@link #remove remove()},
* {@link #clear clear()}, and make the {@link #navigableKeySet navigableKeySet()} and {@link #entrySet entrySet()}
* iterators mutable.
*
*
* All overridden methods must be aware of the {@linkplain #bounds range restriction bounds}, if any.
*
* @param key type
* @param value type
*/
public abstract class AbstractNavigableMap extends AbstractMap implements NavigableMap {
/**
* Key range bounds associated with this instance.
*/
protected final Bounds bounds;
/**
* Convenience constructor for the case where there are no lower or upper key bounds.
*/
protected AbstractNavigableMap() {
this(new Bounds<>());
}
/**
* Primary constructor.
*
* @param bounds key range restriction
* @throws IllegalArgumentException if {@code bounds} is null
*/
protected AbstractNavigableMap(Bounds bounds) {
Preconditions.checkArgument(bounds != null, "null bounds");
this.bounds = bounds;
}
/**
* Get the {@link Bounds} associated with this instance.
*
* @return range restriction
*/
public Bounds getBounds() {
return this.bounds;
}
/**
* Removes the mapping for a key from this map if it is present.
*
*
* The implementation in {@link AbstractNavigableMap} always throws {@link UnsupportedOperationException}.
*/
@Override
public V remove(Object key) {
throw new UnsupportedOperationException("read-only map");
}
@Override
public boolean isEmpty() {
return this.navigableKeySet().isEmpty();
}
@Override
public boolean containsKey(Object obj) {
return this.navigableKeySet().contains(obj);
}
@Override
public K firstKey() {
try (CloseableIterator i = CloseableIterator.wrap(this.navigableKeySet().iterator())) {
return i.next();
}
}
@Override
public K lastKey() {
return this.descendingMap().firstKey();
}
@Override
public NavigableSet keySet() {
return this.navigableKeySet();
}
@Override
public Map.Entry lowerEntry(K maxKey) {
return this.searchBelow(maxKey, false);
}
@Override
public Map.Entry floorEntry(K maxKey) {
return this.searchBelow(maxKey, true);
}
@Override
public Map.Entry ceilingEntry(K minKey) {
return this.searchAbove(minKey, true);
}
@Override
public Map.Entry higherEntry(K minKey) {
return this.searchAbove(minKey, false);
}
@Override
public K lowerKey(K maxKey) {
final Map.Entry entry = this.lowerEntry(maxKey);
return entry != null ? entry.getKey() : null;
}
@Override
public K floorKey(K maxKey) {
final Map.Entry entry = this.floorEntry(maxKey);
return entry != null ? entry.getKey() : null;
}
@Override
public K ceilingKey(K minKey) {
final Map.Entry entry = this.ceilingEntry(minKey);
return entry != null ? entry.getKey() : null;
}
@Override
public K higherKey(K minKey) {
final Map.Entry entry = this.higherEntry(minKey);
return entry != null ? entry.getKey() : null;
}
@Override
public Map.Entry firstEntry() {
try (CloseableIterator> i = CloseableIterator.wrap(this.entrySet().iterator())) {
return i.next();
} catch (NoSuchElementException e) {
return null;
}
}
@Override
public Map.Entry lastEntry() {
return this.descendingMap().firstEntry();
}
@Override
public Map.Entry pollFirstEntry() {
try (CloseableIterator> i = CloseableIterator.wrap(this.entrySet().iterator())) {
if (!i.hasNext())
return null;
final Map.Entry entry = i.next();
i.remove();
return entry;
}
}
@Override
public Map.Entry pollLastEntry() {
return this.descendingMap().pollFirstEntry();
}
@Override
public NavigableMap descendingMap() {
return this.createSubMap(true, this.bounds.reverse());
}
@Override
public NavigableSet descendingKeySet() {
return this.navigableKeySet().descendingSet();
}
@Override
public NavigableMap subMap(K minKey, K maxKey) {
return this.subMap(minKey, true, maxKey, false);
}
@Override
public NavigableMap headMap(K maxKey) {
return this.headMap(maxKey, false);
}
@Override
public NavigableMap tailMap(K minKey) {
return this.tailMap(minKey, true);
}
@Override
public NavigableMap headMap(K newMaxKey, boolean inclusive) {
final Bounds newBounds = this.bounds.withUpperBound(newMaxKey, BoundType.of(inclusive));
if (newBounds.isInverted(this.comparator()))
throw new IllegalArgumentException("upper bound " + newMaxKey + " < lower bound " + this.bounds.getLowerBound());
if (!this.bounds.isWithinBounds(this.comparator(), newBounds))
throw new IllegalArgumentException("upper bound " + newMaxKey + " is out of bounds: " + this.bounds);
return this.createSubMap(false, newBounds);
}
@Override
public NavigableMap tailMap(K newMinKey, boolean inclusive) {
final Bounds newBounds = this.bounds.withLowerBound(newMinKey, BoundType.of(inclusive));
if (newBounds.isInverted(this.comparator()))
throw new IllegalArgumentException("lower bound " + newMinKey + " > upper bound " + this.bounds.getUpperBound());
if (!this.bounds.isWithinBounds(this.comparator(), newBounds))
throw new IllegalArgumentException("lower bound " + newMinKey + " is out of bounds: " + this.bounds);
return this.createSubMap(false, newBounds);
}
@Override
public NavigableMap subMap(K newMinKey, boolean minInclusive, K newMaxKey, boolean maxInclusive) {
final Bounds newBounds = new Bounds<>(newMinKey, BoundType.of(minInclusive), newMaxKey, BoundType.of(maxInclusive));
if (newBounds.isInverted(this.comparator()))
throw new IllegalArgumentException("new bound(s) " + newBounds + " are backwards");
if (!this.bounds.isWithinBounds(this.comparator(), newBounds))
throw new IllegalArgumentException("new bound(s) " + newBounds + " are out of bounds: " + this.bounds);
return this.createSubMap(false, newBounds);
}
/**
* Search for a lower element. Used to implement {@link #floorEntry floorEntry()} and {@link #lowerEntry lowerEntry()}.
*
*
* The implementation in {@link AbstractNavigableMap} checks the bounds then returns the first entry from a head map.
*
* @param maxKey upper limit for search
* @param inclusive true if {@code maxKey} itself is a candidate
* @return highest element below {@code maxKey}, or null if not found
*/
protected Map.Entry searchBelow(K maxKey, boolean inclusive) {
if (!this.isWithinLowerBound(maxKey))
return null;
final NavigableMap subMap = this.isWithinUpperBound(maxKey) ? this.headMap(maxKey, inclusive) : this;
try {
return subMap.lastEntry();
} catch (NoSuchElementException e) {
return null;
}
}
/**
* Search for a higher element. Used to implement {@link #ceilingEntry ceilingEntry()} and {@link #higherEntry higherEntry()}.
*
*
* The implementation in {@link AbstractNavigableMap} checks the bounds then returns the first entry from a tail map.
*
* @param minKey lower limit for search
* @param inclusive true if {@code minKey} itself is a candidate
* @return lowest element above {@code minKey}, or null if not found
*/
protected Map.Entry searchAbove(K minKey, boolean inclusive) {
if (!this.isWithinUpperBound(minKey))
return null;
final NavigableMap subMap = this.isWithinLowerBound(minKey) ? this.tailMap(minKey, inclusive) : this;
try {
return subMap.firstEntry();
} catch (NoSuchElementException e) {
return null;
}
}
/**
* Get a non-null {@link Comparator} that sorts consistently with, and optionally reversed from, this instance.
*
* @param reversed whether to return a reversed {@link Comparator}
* @return a non-null {@link Comparator}
*/
protected Comparator super K> getComparator(boolean reversed) {
return NavigableSets.getComparator(this.comparator(), reversed);
}
/**
* Create a (possibly reversed) view of this instance with (possibly) tighter lower and/or upper bounds.
* The {@code newBounds} are consistent with the new ordering (i.e., reversed relative to this instance's ordering if
* {@code reverse} is true) and have already been range-checked against {@linkplain #bounds this instance's current bounds}.
*
* @param reverse whether the new map's ordering should be reversed relative to this instance's ordering
* @param newBounds new bounds
* @return restricted and/or reversed view of this instance
* @throws IllegalArgumentException if {@code newBounds} is null
* @throws IllegalArgumentException if a bound in {@code newBounds} is null and this set does not permit null elements
*/
protected abstract NavigableMap createSubMap(boolean reverse, Bounds newBounds);
/**
* Determine if the given element is within this instance's lower bound (if any).
*
*
* The implementation in {@link AbstractNavigableMap} returns {@code this.bounds.isWithinLowerBound(this.comparator(), elem)}.
*
* @param key map key
* @return true if {@code elem} is within this instance's lower bound, or this instance has no lower bound
*/
protected boolean isWithinLowerBound(K key) {
return this.bounds.isWithinLowerBound(this.comparator(), key);
}
/**
* Determine if the given element is within this instance's upper bound (if any).
*
*
* The implementation in {@link AbstractNavigableMap} returns {@code this.bounds.isWithinUpperBound(this.comparator(), elem)}.
*
* @param key map key
* @return true if {@code elem} is within this instance's upper bound, or this instance has no upper bound
*/
protected boolean isWithinUpperBound(K key) {
return this.bounds.isWithinUpperBound(this.comparator(), key);
}
}