package com.googlecode.objectify.impl;
import com.google.appengine.api.datastore.Cursor;
import com.google.appengine.api.datastore.FetchOptions;
import com.google.appengine.api.datastore.Index;
import com.google.appengine.api.datastore.PreparedQuery;
import com.google.appengine.api.datastore.QueryResultIterator;
import com.google.common.collect.Iterators;
import com.google.common.collect.PeekingIterator;
import com.googlecode.objectify.Key;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.logging.Logger;
/**
* Base class for normal and hybrid iterators, handles the chunking logic.
*
* The bulk of the complexity is in the QueryResultStreamIterator; this just handles stripping out
* null values but being careful about preserving cursor behavior.
*/
public class ChunkingIterator<T> implements QueryResultIterator<T> {
/** */
static final Logger log = Logger.getLogger(ChunkingIterator.class.getName());
/** Input values */
private PreparedQuery pq;
private QueryResultIterator<Key<T>> source;
/** As we process */
private PeekingIterator<ResultWithCursor<T>> stream;
/** Track the values for the next time we need to get this */
private Cursor nextCursor;
private int nextOffset;
/** */
public ChunkingIterator(LoadEngine loadEngine, PreparedQuery pq, QueryResultIterator<Key<T>> source, int chunkSize) {
this.pq = pq;
this.source = source;
ChunkIterator<T> chunkIt = new ChunkIterator<>(source, chunkSize, loadEngine);
this.stream = Iterators.peekingIterator(Iterators.concat(chunkIt));
// Always start with a cursor; there might actually be any results
this.nextCursor = source.getCursor();
}
@Override
public boolean hasNext() {
while (stream.hasNext()) {
ResultWithCursor<T> peek = stream.peek();
nextCursor = peek.getCursor();
nextOffset = peek.getOffset();
if (peek.getResult() != null)
return true;
else
stream.next();
}
return false;
}
@Override
public T next() {
while (stream.hasNext()) {
ResultWithCursor<T> rc = stream.next();
if (rc.isLast()) {
// We know we are back to the beginning of a batch, and the source cursor should be pointed the right place.
nextCursor = source.getCursor();
nextOffset = 0;
} else {
nextCursor = rc.getCursor();
nextOffset = rc.getOffset() + 1;
}
if (rc.getResult() != null) {
return rc.getResult();
}
}
throw new NoSuchElementException();
}
/** Not implemented */
@Override
public void remove() {
throw new UnsupportedOperationException();
}
/**
* From Alfred Fuller (principal GAE datastore guru):
*
* Calling getCursor() for results in the middle of a batch forces the sdk to run a new query as seen here:
* http://code.google.com/p/googleappengine/source/browse/trunk/java/src/main/com/google/appengine/api/datastore/Cursor.java#70
*
* Doing this for every result will definitely give you really bad performance. I have several yet to be implemented ideas
* that would solve this problem (which you potentially could push me into prioritizing), but I believe you can solve the
* performance problem today by saving the start_cursor an offset into the batch. Then you can evaluate the real cursor on
* demand using "query.asQueryResultIterator(withStartCursor(cursor).offset(n).limit(0)).getCursor()"
*/
@Override
public Cursor getCursor() {
if (nextOffset == 0) {
return nextCursor;
} else {
// There may not be a baseCursor if we haven't iterated yet
FetchOptions opts = FetchOptions.Builder.withDefaults();
if (nextCursor != null)
opts = opts.startCursor(nextCursor);
return pq.asQueryResultIterator(opts.offset(nextOffset).limit(0)).getCursor();
}
}
@Override
public List<Index> getIndexList() {
return this.source.getIndexList();
}
}