UrlEncodedDataGetter.java

package com.renomad.minum.web;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * This class enables pulling key-value pairs one at a time
 * from the Request body. This enables the developer to pull data
 * incrementally, rather than reading it all into memory at once.
 */
public class UrlEncodedDataGetter extends InputStream {
    private final InputStream inputStream;
    private final CountBytesRead countBytesRead;
    private final long contentLength;
    /**
     * After we hit the boundary, we will set this flag to true, and all
     * subsequent reads will return -1.
     */
    private boolean isFinished = false;

    UrlEncodedDataGetter(InputStream inputStream, CountBytesRead countBytesRead, long contentLength) {
        this.inputStream = inputStream;
        this.countBytesRead = countBytesRead;
        this.contentLength = contentLength;
    }

    /**
     * Mostly similar behavior to {@link InputStream#read()}
     * @throws IOException if the inputstream is closed unexpectedly while reading.
     */
    @Override
    public int read() throws IOException {
        if (isFinished) {
            return -1;
        }
        if (countBytesRead.getCount() == contentLength) {
            isFinished = true;
            return -1;
        }
        int result = inputStream.read();

        if (result == -1) {
            isFinished = true;
            // I know this is surprising, however: Because we always have the content length while reading the body,
            // we know exactly when we expect to read the last byte.  If we read and get a -1, it means
            // the stream is closed - but that should not have happened, because we should have stopped reading when
            // we hit the limit of bytes to read.  But in the real world, it will happen:  You can observe it by uploading a large
            // file and using the browser's "stop" button during the upload.
            throw new IOException("Error: The inputstream has closed unexpectedly while reading");
        }

        countBytesRead.increment();
        char byteValue = (char) result;
        if (byteValue == '&') {
            isFinished = true;
            return -1;
        }
        return result;
    }

    @Override
    public byte[] readAllBytes() throws IOException {
        var baos = new ByteArrayOutputStream();
        while (true) {
            int result = read();
            if (result == -1) {
                // if our read function determines we are at the end of the value,
                // because we encountered an ampersand, it will return a -1 value,
                // and we return our value - but more keys and values expected.
                return baos.toByteArray();
            }
            baos.write((byte)result);
        }
    }

    /**
     * By "close", we will read from the {@link InputStream} until we have finished the body,
     * so that our InputStream has been read until the start of the next partition.
     */
    @Override
    public void close() throws IOException {
        while (true) {
            int result = read();
            if (result == -1) {
                return;
            }
        }
    }
}