All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.kolibrifx.plovercrest.server.internal.folds.AbstractPeekableInputStream Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2010-2017, KolibriFX AS. Licensed under the Apache License, version 2.0.
 */

package com.kolibrifx.plovercrest.server.internal.folds;

import com.kolibrifx.plovercrest.server.internal.streams.NullStreamObserver;
import com.kolibrifx.plovercrest.server.streams.PollableReader;
import com.kolibrifx.plovercrest.server.streams.Stream;
import com.kolibrifx.plovercrest.server.streams.StreamDataTriggerer;
import com.kolibrifx.plovercrest.server.streams.StreamObserver;

/**
 * Base implementation of a peekable stream. Subclasses need to specify how to convert from the
 * input to output type.
 * 
 * @param 
 *            The output type.
 * @param 
 *            The input stream type.
 */
public abstract class AbstractPeekableInputStream implements PeekableInputStream {
    private PollableReader reader = null;
    private long peekTimestamp = Long.MIN_VALUE;
    private boolean hasPeekData = false;
    private T peekElement = null;
    private boolean reachedEnd = false;
    private final long fromTimestamp;
    private final Stream stream;

    private final StreamObserver streamObserver = new StreamObserver() {
        @Override
        public void onObserve(final long timestamp, final long index, final U element) {
            hasPeekData = true;
            peekTimestamp = timestamp;
            peekElement = convertElement(element);
            reachedEnd = false;
        }

        @Override
        public void onObserveEnd(final long lastValidTimestamp, final long elementCount) {
            reachedEnd = true;
        }

        @Override
        public void onObserveFrozen() {
        }
    };

    public AbstractPeekableInputStream(final Stream stream, final long fromTimestamp) {
        this.stream = stream;
        this.fromTimestamp = fromTimestamp;
    }

    protected abstract T convertElement(U element);

    private void tryPeekNext() {
        if (hasPeekData) {
            return;
        }
        if (reader == null) {
            reader = stream.createReaderFromTimestamp(fromTimestamp, streamObserver);
            if (stream.getLastTimestamp() < fromTimestamp) {
                // The stream does not (yet) have enough data to start. We abort, but also need to make sure the
                // data triggerer wakes us up when more data is available. This is done by reading to the end,
                // then registering a data triggerer, unless we suddenly have enough data available anyway.
                // This logic is a bit tricky, but should work.
                boolean shouldAbort = true;
                while (reader.poll()) {
                    if (stream.getLastTimestamp() >= fromTimestamp) {
                        // Enough data became available while polling, so we don't have to abort anyway.
                        // Just create a new reader, and we're done.
                        reader = stream.createReaderFromTimestamp(fromTimestamp, streamObserver);
                        shouldAbort = false;
                        break;
                    }
                }
                if (shouldAbort) {
                    hasPeekData = false;
                    reader = null;
                    return;
                }
            }
        }
        final boolean r = reader.poll();
        assert r == !reachedEnd;
    }

    @Override
    public boolean hasNext() {
        tryPeekNext();
        return hasPeekData;
    }

    @Override
    public long nextTimestamp() {
        if (hasPeekData) {
            return peekTimestamp;
        } else {
            throw new IllegalStateException("No next timestamp, only call if hasNext() is true");
        }
    }

    @Override
    public void consumeNext(final FoldCallback cb) {
        if (hasPeekData) {
            cb.onNext(stream.getName(), peekTimestamp, peekElement);
            hasPeekData = false;
        } else {
            throw new IllegalStateException("Nothing to consume, only call if hasNext() is true");
        }
    }

    @Override
    public boolean registerDataTriggererIfReachedEnd(final StreamDataTriggerer dataTriggerer) {
        if (hasPeekData || !reachedEnd) {
            return false;
        }
        if (reader == null) {
            // This happens if the stream did not have enough data to start when tryPeekNext() was last called.
            if (stream.getLastTimestamp() >= fromTimestamp) {
                // Enough data is available now
                dataTriggerer.wakeUpNow();
                return true;
            }
            // Create a temporary reader, poll until the end is reached, then register the data triggerer.
            final PollableReader tmpReader =
                    stream.createReaderFromTimestamp(fromTimestamp, new NullStreamObserver());
            while (tmpReader.poll()) {
                if (stream.getLastTimestamp() >= fromTimestamp) {
                    // Enough data became available while polling.
                    dataTriggerer.wakeUpNow();
                    return true;
                }
            }
            tmpReader.registerDataTriggerer(dataTriggerer);
        } else {
            reader.registerDataTriggerer(dataTriggerer);
        }
        return true;
    }

}