MultiInputStream.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.InputStream;
import java.util.Iterator;
import java.util.Optional;
import edu.umd.cs.findbugs.annotations.Nullable;

/**
 * A {@code MultiInputStream} concatenates multiple {@link InputStream}s into
 * one. That might be useful for e.g. reading log files of multiple days.
 *
 * <p>
 * The {@code inputStreamIterator} needs to supply the {@link InputStream}s to
 * concatenate one after the other. {@link InputStream}s, which have been read,
 * are closed prior requesting the next.
 */
public class MultiInputStream extends InputStream {
	/**
	 * Value, that is returned by {@link InputStream#read()} on end of an
	 * {@link InputStream}.
	 */
	private static final int END_OF_INPUT_STREAM = -1;
	/**
	 * If {@code true} this {@link MultiInputStream} reached its end, meaning
	 * {@link #inputStreamIterator} has no next element.
	 */
	private boolean closed = false;
	/**
	 * The current {@link InputStream} or empty if the next needs to be supplied
	 * first or if this {@link MultiInputStream} is closed.
	 */
	private Optional<InputStream> currentInputStream = Optional.empty();
	/**
	 * {@link Iterator}, called whenever the next {@link InputStream} needs to be
	 * supplied. If it has no next element, the {@link MultiInputStream} is closed.
	 */
	private final Iterator<InputStream> inputStreamIterator;

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void close() throws IOException {
		closed = true;
		if (currentInputStream.isPresent()) {
			currentInputStream.get().close();
			currentInputStream = Optional.empty();
		}
	}

	/**
	 * Determines the current {@link InputStream} by updating the internal state of
	 * this {@link MultiInputStream} if necessary.
	 *
	 * @return the current {@link InputStream} or empty if closed
	 */
	@SuppressWarnings({"checkstyle:SuppressWarnings", "resource"})
	private Optional<InputStream> getCurrentInputStream() {
		if (closed) {
			return Optional.empty();
		}
		if (currentInputStream.isPresent()) {
			return currentInputStream;
		}
		currentInputStream = inputStreamIterator.hasNext() ? Optional.of(inputStreamIterator.next()) : Optional.empty();
		if (!currentInputStream.isPresent()) {
			closed = true;
		}
		return currentInputStream;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	@SuppressWarnings("checkstyle:SuppressWarnings")
	public int read() throws IOException {
		final Optional<InputStream> inputStream = getCurrentInputStream();
		if (!inputStream.isPresent()) {
			return END_OF_INPUT_STREAM;
		}
		// Read from current input stream
		@SuppressWarnings("resource")
		final int value = inputStream.get().read();
		if (value != END_OF_INPUT_STREAM) {
			return value;
		}
		// At the end of the current input stream, close it and continue with the next
		currentInputStream = Optional.empty();
		inputStream.get().close();
		return read();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	@SuppressWarnings("checkstyle:SuppressWarnings")
	public int read(@Nullable final byte[] outputBuffer, final int offset, final int length) throws IOException {
		final Optional<InputStream> inputStream = getCurrentInputStream();
		if (!inputStream.isPresent()) {
			return END_OF_INPUT_STREAM;
		}
		// Read from current input stream
		@SuppressWarnings("resource")
		final int chars = inputStream.get().read(outputBuffer, offset, length);
		if (chars == length) {
			return chars;
		}
		// If less bytes than expected have been read, closing the current input stream
		// and continuing with the next
		currentInputStream = Optional.empty();
		inputStream.get().close();
		// Read additional chunk of bytes
		final int nonNegativeChars = chars == END_OF_INPUT_STREAM ? 0 : chars;
		final int nextChars = read(outputBuffer, offset + nonNegativeChars, length - nonNegativeChars);
		return nonNegativeChars + (nextChars == END_OF_INPUT_STREAM ? 0 : nextChars);
	}

	/**
	 * Creates a new {@code MultiInputStream} instance.
	 *
	 * @param inputStreamIterator {@link Iterator}, called whenever the next {@link InputStream} needs to be
	 * supplied. If it has no next element, the {@link MultiInputStream} is closed.
	 */
	@java.lang.SuppressWarnings("all")
	@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(justification = "generated code")
	@lombok.Generated
	public MultiInputStream(final Iterator<InputStream> inputStreamIterator) {
		this.inputStreamIterator = inputStreamIterator;
	}
}