org.jsimpledb.kv.util.AbstractKVNavigableSet Maven / Gradle / Ivy
/*
* Copyright (C) 2015 Archie L. Cobbs. All rights reserved.
*/
package org.jsimpledb.kv.util;
import com.google.common.base.Preconditions;
import java.util.Iterator;
import java.util.NavigableSet;
import org.jsimpledb.kv.KVPair;
import org.jsimpledb.kv.KVStore;
import org.jsimpledb.kv.KeyFilter;
import org.jsimpledb.kv.KeyFilterUtil;
import org.jsimpledb.kv.KeyRange;
import org.jsimpledb.util.AbstractNavigableSet;
import org.jsimpledb.util.Bounds;
import org.jsimpledb.util.ByteReader;
import org.jsimpledb.util.ByteUtil;
import org.jsimpledb.util.ByteWriter;
/**
* {@link java.util.NavigableSet} support superclass for sets backed by elements encoded as {@code byte[]}
* keys in a {@link KVStore} and whose sort order is consistent with their {@code byte[]} key encoding.
* There must be an equivalence between elements and {@code byte[]} key encodings (i.e., there must be
* only one valid encoding per set element).
*
* Subclass Methods
*
*
* Subclasses must implement the {@linkplain #encode(ByteWriter, Object) encode()} and {@linkplain #decode decode()}
* methods to convert elements to/from {@code byte[]} keys (associated values are ignored), and
* {@link #createSubSet(boolean, KeyRange, KeyFilter, Bounds) createSubSet()}
* to allow creating reversed and restricted range sub-sets.
*
*
* Subclasses must also implement {@link #comparator comparator()}, and the resulting sort order must be consistent with
* the sort order of the encoded {@code byte[]} keys (possibly {@link #reversed}).
*
*
* This class provides a read-only implementation; for a mutable implementation, subclasses should also implement
* {@link #add add()} (if appropriate), {@link #remove remove()}, and {@link #clear}; note, these methods must verify
* the key {@link #isVisible isVisible()} before making any changes.
*
*
* Additional subclass notes:
*
* - {@link #iterator} returns an {@link Iterator} whose {@link Iterator#remove Iterator.remove()} method delegates
* to this instance's {@link #remove remove()} method.
*
*
* Prefix Mode
*
*
* Instances support a "prefix mode" where the {@code byte[]} keys may have arbitrary trailing garbage, which is ignored,
* and so by definition no key can be a prefix of any other key. The length of the prefix is determined implicitly by the
* number of bytes produced by {@link #encode encode()} or consumed by {@link #decode decode()}.
* When not in prefix mode, {@link #decode decode()} must consume the entire key to preserve correct semantics.
*
*
Key Restrictions
*
*
* Instances are configured with an (optional) {@link KeyRange}; when {@linkplain #bounds range restriction} is in
* effect, this key range corresponds to the bounds.
*
*
* Instances also support filtering visible values using a {@link KeyFilter}; see {@link #filterKeys filterKeys()}.
* To be {@linkplain #isVisible} in the set, keys must both be in the {@link KeyRange} and pass the {@link KeyFilter}.
*
*
Concurrent Modifications
*
*
* This implementation never throws {@link java.util.ConcurrentModificationException}; instead, iterators always
* see the most up-to-date state of the associated {@link KVStore}.
*
* @see AbstractKVNavigableMap
* @param element type
*/
public abstract class AbstractKVNavigableSet extends AbstractNavigableSet {
/**
* The underlying {@link KVStore}.
*/
protected final KVStore kv;
/**
* Whether we are in "prefix" mode.
*/
protected final boolean prefixMode;
/**
* Whether the ordering of this instance is reversed.
*/
protected final boolean reversed;
/**
* Key range, or null for the entire range.
*/
protected final KeyRange keyRange;
/**
* Key filter, or null if all keys in the key range should be visible.
*/
protected final KeyFilter keyFilter;
// Constructors
/**
* Convenience constructor for when there are no range restrictions.
*
* @param kv underlying {@link KVStore}
* @param prefixMode whether to allow keys to have trailing garbage
* @throws IllegalArgumentException if {@code kv} is null
*/
protected AbstractKVNavigableSet(KVStore kv, boolean prefixMode) {
this(kv, prefixMode, (KeyRange)null);
}
/**
* Convenience constructor for when the range of visible {@link KVStore} keys is all keys sharing a given {@code byte[]} prefix.
*
* @param kv underlying {@link KVStore}
* @param prefixMode whether to allow keys to have trailing garbage
* @param prefix prefix defining minimum and maximum keys
* @throws IllegalArgumentException if {@code kv} is null
* @throws IllegalArgumentException if {@code prefix} is null or empty
*/
protected AbstractKVNavigableSet(KVStore kv, boolean prefixMode, byte[] prefix) {
this(kv, prefixMode, KeyRange.forPrefix(prefix));
}
/**
* Primary constructor.
*
* @param kv underlying {@link KVStore}
* @param prefixMode whether to allow keys to have trailing garbage
* @param keyRange key range restriction, or null for none
* @throws IllegalArgumentException if {@code kv} is null
*/
protected AbstractKVNavigableSet(KVStore kv, boolean prefixMode, KeyRange keyRange) {
this(kv, prefixMode, false, keyRange, null, new Bounds());
}
/**
* Internal constructor. Used for creating sub-sets and reversed views.
*
*
* Note: if {@code bounds} are set, then {@code keyRange} must exclude all keys outside of those bounds.
*
* @param kv underlying {@link KVStore}
* @param prefixMode whether to allow keys to have trailing garbage
* @param reversed whether ordering is reversed (implies {@code bounds} are also inverted, but not {@code keyRange})
* @param keyRange key range restriction, or null for none
* @param keyFilter key filter, or null for none
* @param bounds range restriction
* @throws IllegalArgumentException if {@code kv} or {@code bounds} is null
*/
protected AbstractKVNavigableSet(KVStore kv, boolean prefixMode, boolean reversed,
KeyRange keyRange, KeyFilter keyFilter, Bounds bounds) {
super(bounds);
Preconditions.checkArgument(kv != null, "null kv");
this.kv = kv;
this.prefixMode = prefixMode;
this.reversed = reversed;
this.keyRange = keyRange;
this.keyFilter = keyFilter;
}
@Override
@SuppressWarnings("unchecked")
public boolean contains(Object obj) {
// Encode key and check visibility
final byte[] key = this.encodeVisible(obj, false);
if (key == null)
return false;
// Find key, or some longer key with the same prefix in prefix mode
final KVPair pair;
if (this.prefixMode) {
pair = this.kv.getAtLeast(key);
return pair != null && ByteUtil.isPrefixOf(key, pair.getKey());
} else
return this.kv.get(key) != null;
}
@Override
public Iterator iterator() {
return new AbstractKVIterator(this.kv, this.prefixMode, this.reversed, this.keyRange, this.keyFilter) {
@Override
protected E decodePair(KVPair pair, ByteReader keyReader) {
return AbstractKVNavigableSet.this.decode(keyReader);
}
@Override
protected void doRemove(E value, KVPair pair) {
AbstractKVNavigableSet.this.remove(value);
}
};
}
@Override
public boolean remove(Object obj) {
throw new UnsupportedOperationException();
}
/**
* Create a view of this instance with additional filtering applied to the underlying {@code byte[]} encoded keys.
* Any set element for which the corresponding key does not pass {@code keyFilter} will be effectively hidden from view.
*
*
* The restrictions of the given {@link KeyFilter} will be added to any current {@link KeyFilter} restrictions on this instance.
* The {@link #bounds} associated with this instance will not change.
*
* @param keyFilter additional key filtering to apply
* @return filtered view of this instance
* @throws IllegalArgumentException if {@code keyFilter} is null
*/
public NavigableSet filterKeys(KeyFilter keyFilter) {
Preconditions.checkArgument(keyFilter != null, "null keyFilter");
if (this.keyFilter != null)
keyFilter = KeyFilterUtil.intersection(keyFilter, this.keyFilter);
return this.createSubSet(this.reversed, this.keyRange, keyFilter, this.bounds);
}
@Override
protected boolean isWithinLowerBound(E elem) {
if (!super.isWithinLowerBound(elem))
return false;
if (this.keyRange == null)
return true;
final ByteWriter writer = new ByteWriter();
this.encode(writer, elem);
return KeyRange.compare(writer.getBytes(), this.keyRange.getMin()) >= 0;
}
@Override
protected boolean isWithinUpperBound(E elem) {
if (!super.isWithinUpperBound(elem))
return false;
if (this.keyRange == null)
return true;
final ByteWriter writer = new ByteWriter();
this.encode(writer, elem);
return KeyRange.compare(writer.getBytes(), this.keyRange.getMax()) < 0;
}
@Override
protected final NavigableSet createSubSet(boolean reverse, Bounds newBounds) {
// Determine the direction of the new sub-set
final boolean newReversed = this.reversed ^ reverse;
// Determine new min and max keys
final KeyRange newKeyRange = this.buildKeyRange(newReversed ? newBounds.reverse() : newBounds);
// Create subset
return this.createSubSet(newReversed, newKeyRange, this.keyFilter, newBounds);
}
/**
* Create a (possibly reversed) view of this instance with (possibly) tighter lower and/or upper bounds and
* the given {@link KeyFilter}, if any.
* The bounds are consistent with the reversed ordering (i.e., reversed if {@code reverse} is true)
* and have already been range-checked against this instance's bounds.
*
* @param newReversed whether the new set's ordering should be reversed (implies {@code newBounds} are also inverted,
* but not {@code keyRange}); note: means "absolutely" reversed, not relative to this instance
* @param newKeyRange new key range, or null for none; will be consistent with {@code bounds}, if any
* @param newKeyFilter new key filter, or null for none
* @param newBounds new bounds
* @return restricted and/or filtered view of this instance
* @throws IllegalArgumentException if {@code newBounds} is null
*/
protected abstract NavigableSet createSubSet(boolean newReversed,
KeyRange newKeyRange, KeyFilter newKeyFilter, Bounds newBounds);
/**
* Encode the given object into a {@code byte[]} key.
* Note that this method must throw {@link IllegalArgumentException}, not {@link ClassCastException}
* or {@code NullPointerException}, if {@code obj} does not have the correct type or is an illegal null value.
*
* @param writer output for encoded {@code byte[]} key corresponding to {@code obj}
* @param obj set element
* @throws IllegalArgumentException if {@code obj} is not of the required Java type supported by this set
* @throws IllegalArgumentException if {@code obj} is null and this set does not support null elements
*/
protected abstract void encode(ByteWriter writer, Object obj);
/**
* Decode an element from a {@code byte[]} key.
*
*
* If not in prefix mode, all of {@code reader} must be consumed; otherwise, the consumed portion
* is the prefix and any following keys with the same prefix are ignored.
*
* @param reader input for encoded bytes
* @return decoded set element
*/
protected abstract E decode(ByteReader reader);
/**
* Determine if the given {@code byte[]} key is visible in this set according to the configured
* {@link KeyRange} and/or {@link KeyFilter}, if any.
*
* @param key key to test
* @return true if key is visible
* @throws IllegalArgumentException if {@code key} is null
* @see #filterKeys filterKeys()
*/
protected boolean isVisible(byte[] key) {
return (this.keyRange == null || this.keyRange.contains(key))
&& (this.keyFilter == null || this.keyFilter.contains(key));
}
/**
* Encode the given object, if possible, and verify corresponding {@code byte[]} key is visible,
* otherwise return null or throw an exception.
* Delegates to {@link #encode(ByteWriter, Object)} to attempt the actual encoding.
*
* @param obj object to encode, possibly null
* @param fail whether, if {@code obj} can't be encoded, to throw an exception (true) or return null (false)
* @return encoed key for {@code obj}, or null if {@code fail} is false and {@code obj} has the wrong type or is out of bounds
* @throws IllegalArgumentException if {@code fail} is true and {@code obj} has the wrong type
* @throws IllegalArgumentException if {@code fail} is true and the resulting key is not {@linkplain #isVisible visible}
*/
protected byte[] encodeVisible(Object obj, boolean fail) {
final ByteWriter writer = new ByteWriter();
try {
this.encode(writer, obj);
} catch (IllegalArgumentException e) {
if (!fail)
return null;
throw e;
}
final byte[] key = writer.getBytes();
if (this.keyRange != null && !this.keyRange.contains(key)) {
if (fail)
throw new IllegalArgumentException("value is out of bounds: " + obj);
return null;
}
if (this.keyFilter != null && !this.keyFilter.contains(key)) {
if (fail)
throw new IllegalArgumentException("value is filtered out: " + obj);
return null;
}
return key;
}
/**
* Derive a new {@link KeyRange} from (possibly) new element bounds. The given bounds must not ever be reversed.
*/
private KeyRange buildKeyRange(Bounds bounds) {
final byte[] minKey = this.keyRange != null ? this.keyRange.getMin() : null;
final byte[] maxKey = this.keyRange != null ? this.keyRange.getMax() : null;
byte[] newMinKey;
byte[] newMaxKey;
switch (bounds.getLowerBoundType()) {
case NONE:
newMinKey = minKey;
break;
default:
final ByteWriter writer = new ByteWriter();
this.encode(writer, bounds.getLowerBound());
newMinKey = writer.getBytes();
if (!bounds.getLowerBoundType().isInclusive())
newMinKey = this.prefixMode ? ByteUtil.getKeyAfterPrefix(newMinKey) : ByteUtil.getNextKey(newMinKey);
if (minKey != null)
newMinKey = ByteUtil.max(newMinKey, minKey);
if (maxKey != null)
newMinKey = ByteUtil.min(newMinKey, maxKey);
break;
}
switch (bounds.getUpperBoundType()) {
case NONE:
newMaxKey = maxKey;
break;
default:
final ByteWriter writer = new ByteWriter();
this.encode(writer, bounds.getUpperBound());
newMaxKey = writer.getBytes();
if (bounds.getUpperBoundType().isInclusive())
newMaxKey = this.prefixMode ? ByteUtil.getKeyAfterPrefix(newMaxKey) : ByteUtil.getNextKey(newMaxKey);
if (maxKey != null)
newMaxKey = ByteUtil.min(newMaxKey, maxKey);
if (minKey != null)
newMaxKey = ByteUtil.max(newMaxKey, minKey);
break;
}
return new KeyRange(newMinKey != null ? newMinKey : ByteUtil.EMPTY, newMaxKey);
}
}