PeekableReader.java

// Generated by delombok at Mon Nov 18 07:27:48 UTC 2024
package de.larssh.utils.io;

import java.io.IOException;
import java.io.Reader;
import java.util.NoSuchElementException;
import de.larssh.utils.Nullables;
import edu.umd.cs.findbugs.annotations.Nullable;

/**
 * A {@link Reader} that allows peeking up to one character without removing it,
 * from the logical I/O stream, supporting a one-element lookahead.
 *
 * <p>
 * The methods {@link #hasNext()} and {@link #next()} are implemented similar to
 * the methods of {@link java.util.Iterator} to simplify working with character
 * based readers.
 */
public class PeekableReader extends Reader {
	/**
	 * The wrapped reader
	 */
	private final Reader reader;
	/**
	 * The next character read if {@link #state} is {@link ReaderState#PEEKED}, else
	 * undefined.
	 */
	private int peekedCharacter = -1;
	/**
	 * The reader's current inner state
	 */
	private ReaderState state = ReaderState.CALL_FOR_NEXT;
	/**
	 * The peeked character at the time of calling {@link #mark(int)} the last time.
	 */
	private int markedPeekedCharacter = -1;
	/**
	 * The reader's current inner state at the time of calling {@link #mark(int)}
	 * the last time.
	 */
	private ReaderState markedState = ReaderState.CALL_FOR_NEXT;

	/**
	 * {@inheritDoc}
	 */
	@Override
	@SuppressWarnings("PMD.CloseResource")
	public void close() throws IOException {
		reader.close();
	}

	/**
	 * Returns {@code true} if the reader has more characters. (In other words,
	 * returns {@code true} if {@link #next()} would return a character rather than
	 * throwing an exception.)
	 *
	 * @return {@code true} if the reader has more characters, else {@code false}
	 * @throws IOException if an I/O error occurs
	 */
	public final boolean hasNext() throws IOException {
		if (state == ReaderState.CALL_FOR_NEXT) {
			peekedCharacter = reader.read();
			state = peekedCharacter == -1 ? ReaderState.END_OF_DATA : ReaderState.PEEKED;
		}
		return state == ReaderState.PEEKED;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void mark(final int readAheadLimit) throws IOException {
		reader.mark(readAheadLimit);
		markedPeekedCharacter = peekedCharacter;
		markedState = state;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public boolean markSupported() {
		return reader.markSupported();
	}

	/**
	 * Returns the next character of the reader.
	 *
	 * @return the next character of the reader
	 * @throws IOException            if an I/O error occurs
	 * @throws NoSuchElementException if the reader has no more characters
	 */
	public final char next() throws IOException {
		// Method "hasNext" peeks the next character (if required)
		if (!hasNext()) {
			throw new NoSuchElementException();
		}
		if (state == ReaderState.PEEKED) {
			state = ReaderState.CALL_FOR_NEXT;
		}
		final char next = (char) peekedCharacter;
		peekedCharacter = -1;
		return next;
	}

	/**
	 * Returns the next character in the reader, returned by {@link #read()},
	 * without removing it from the I/O stream.
	 *
	 * @return the next element in the iteration
	 * @throws IOException            if an I/O error occurs
	 * @throws NoSuchElementException if the iteration has no more elements
	 */
	public char peek() throws IOException {
		// Method "hasNext" peeks the next character (if required)
		if (!hasNext()) {
			throw new NoSuchElementException();
		}
		return (char) peekedCharacter;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public int read() throws IOException {
		if (state == ReaderState.PEEKED) {
			state = ReaderState.CALL_FOR_NEXT;
			return (char) peekedCharacter;
		}
		return super.read();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	@SuppressWarnings("PMD.CyclomaticComplexity")
	public int read(@Nullable final char[] buffer, final int offset, final int length) throws IOException {
		if (state == ReaderState.CALL_FOR_NEXT) {
			return reader.read(buffer, offset, length);
		}
		if (state == ReaderState.END_OF_DATA) {
			return -1;
		}
		// Error handling as of JavaDoc
		final char[] nonNullableBuffer = Nullables.orElseThrow(buffer);
		if (offset < 0 || length < 0 || offset + length > nonNullableBuffer.length) {
			throw new IndexOutOfBoundsException();
		}
		// Early exit: Avoid further processing in case no character were requested
		if (length == 0) {
			return 0;
		}
		// Insert peeked character as first character
		nonNullableBuffer[offset] = (char) peekedCharacter;
		state = ReaderState.CALL_FOR_NEXT;
		// No need to read further characters if just one character were requested
		if (length == 1) {
			return 1;
		}
		// Read further characters and handle possible end of data
		final int noOfCharacters = reader.read(buffer, offset + 1, length - 1);
		if (noOfCharacters == -1) {
			state = ReaderState.END_OF_DATA;
			return 1;
		}
		return noOfCharacters + 1;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void reset() throws IOException {
		reader.reset();
		this.peekedCharacter = markedPeekedCharacter;
		state = markedState;
	}


	/**
	 * This enumeration contains the possible inner states of
	 * {@link PeekableReader}.
	 */
	@SuppressWarnings("PMD.UnnecessaryModifier")
	private enum ReaderState {
		/**
		 * Status of a reader, that needs to read once information about the next
		 * character is required.
		 */
		CALL_FOR_NEXT, /**
		 * Status representing the end of data. The reader must not be called any
		 * longer. This is an end state and must not change once reached.
		 */
		END_OF_DATA, /**
		 * Status of a reader, that peeked the next character already.
		 */
		PEEKED;
	}

	/**
	 * Creates a new {@code PeekableReader} instance.
	 *
	 * @param reader The wrapped reader
	 */
	@java.lang.SuppressWarnings("all")
	@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(justification = "generated code")
	@lombok.Generated
	public PeekableReader(final Reader reader) {
		this.reader = reader;
	}
}