package org.commcare.models.database;
import android.database.Cursor;
import org.javarosa.core.services.storage.Persistable;
/**
* A index spanning iterator is a special kind of iterator that is used on densely packed tables
* in order to not have to maintain a specific value for each ID.
* <p/>
* If you have the ID set
* [1, 2, 3, 5, 6, 7, 10, 11]
* <p/>
* in your table, this iterator produces those id's from the return set
* [4, 8, 9, 12]
* <p/>
* which is notable less verbose for densely packed datasets.
* <p/>
* This cursor is not threadsafe.
*
* @author ctsims
*/
public class IndexSpanningIterator<T extends Persistable> extends SqlStorageIterator<T> {
private final SqlStorage<T> storage;
private boolean isClosedByProgress = false;
/**
* Total expected records
*/
private final int count;
/**
* The largest integer that is _not_ included in the walk *
*/
private int end;
/**
* The walker. Must be an integer which is included in the result set
* while the iterator is still valid/open.
*/
private int current;
/**
* The next integer which will _not_ be included in the result set
*/
private int nextGap;
/**
* Create an iterator that will walk and return Id's between minValue (inclusive) and max value (exclusive)
* <p/>
* The input cursor of gaps should contain an entry for the last ID to be excluded, and should be ordered
* <p/>
* if the cursor is empty, no ids are iterated over.
*
* @param c - A cursor set with one column of integer values, which are gaps between minValue and maxValue.
* @param storage - The storage being iterated over
* @param minValue - The first (lowest) id value to be included in the result set. Must be smaller than the first gap
* @param maxValue - The last (largest) id value to be included in the result set
* @param countValue - The number of ID's in the set
*/
public IndexSpanningIterator(Cursor c, SqlStorage<T> storage, int minValue, int maxValue, int countValue) {
super(c);
current = minValue;
end = maxValue;
count = countValue;
this.storage = storage;
//If there's no input, there's no values to iterate over
if (!c.moveToNext()) {
current = end = nextGap = -1;
isClosedByProgress = true;
c.close();
} else {
//Otherwise our next gap is the first cursor record
nextGap = c.getInt(0);
}
}
@Override
public boolean hasMore() {
//See whether we're ahead of the next gap. If we are, there are valid
//ids remaining
return nextGap > current;
}
/**
* When currently on a gap, move the current point up to another valid
* entry if possible.
* <p/>
* This method will either result in the iterator pointing to a new valid
* record, or being closed.
*/
private void expandToGap() {
//If this is closed, we can't grow anymore
if (isClosedByProgress) {
return;
}
//otherwise we need to assume that we're at the gap,
//otherwise we shouldn't be growing
if (nextGap != current) {
return;
}
//Assume the current id is invalid (IE: points at a gap)
boolean currentIsValid = false;
//current points at a gap now, which means that it must
//be invalid. Either we will get current into a valid
//state or the iterator needs to close
while (c.moveToNext()) {
int upcomingGap = c.getInt(0);
if (nextGap + 1 == upcomingGap) {
//Adjacent gaps with no valid records, we just
//want to keep going
nextGap = upcomingGap;
} else {
//Otherwise we know that the next record must be valid unless
//the next gap is larger than the end
if (upcomingGap > end) {
nextGap = upcomingGap;
break;
}
//the step after next gap isn't the upcoming gap so it
//must be valid
current = nextGap + 1;
//Set the next gap to be the upcoming one (after current)
nextGap = upcomingGap;
//Mark the iterator's progress as valid, since we know the current
//record exists and there is a gap set
currentIsValid = true;
break;
}
}
//If we didn't end up in a valid state it means that this iterator is done,
//so we should clean up the cursor and signal that.
if (!currentIsValid) {
c.close();
this.isClosedByProgress = true;
current = nextGap;
}
}
@Override
public int nextID() {
int ret = current;
current++;
//If we just hit the gap we need
//to either grow it or we need the iterator
//to terminate
if (current == nextGap) {
expandToGap();
}
return ret;
}
@Override
public T nextRecord() {
T t = storage.read(this.peekID());
nextID();
return t;
}
@Override
public int numRecords() {
return count;
}
@Override
public boolean hasNext() {
return hasMore();
}
@Override
public T next() {
return nextRecord();
}
@Override
public void remove() {
throw new UnsupportedOperationException("Remove() is unsupported by IndexSpanningIterator objects");
}
@Override
public int peekID() {
return current;
}
@Override
public String getPrimaryId() {
throw new RuntimeException("Primary ID Not requested by this iterator");
}
}