Request.java

package com.renomad.minum.web;

import java.util.Objects;

public final class Request implements IRequest {

    private final Headers headers;
    private final RequestLine requestLine;
    private Body body;
    private final String remoteRequester;
    private final ISocketWrapper socketWrapper;
    private final IBodyProcessor bodyProcessor;
    private boolean hasStartedReadingBody;

    /**
     * Constructor for a HTTP request
     * @param  remoteRequester This is the remote address making the request
     */
    public Request(Headers headers,
            RequestLine requestLine,
            String remoteRequester,
            ISocketWrapper socketWrapper,
            IBodyProcessor bodyProcessor
    ) {
        this.headers = headers;
        this.requestLine = requestLine;
        this.remoteRequester = remoteRequester;
        this.socketWrapper = socketWrapper;
        this.bodyProcessor = bodyProcessor;
        this.hasStartedReadingBody = false;
    }

    @Override
    public Headers getHeaders() {
        return headers;
    }

    @Override
    public RequestLine getRequestLine() {
        return requestLine;
    }

    @Override
    public Body getBody() {
        if (hasStartedReadingBody) {
            throw new WebServerException("The InputStream in Request has already been accessed for reading, preventing body extraction from stream." +
                    " If intending to use getBody(), use it exclusively");
        }
        if (body == null) {
            body = bodyProcessor.extractData(socketWrapper.getInputStream(), headers);
        }
        return body;
    }

    @Override
    public String getRemoteRequester() {
        return remoteRequester;
    }

    @Override
    public ISocketWrapper getSocketWrapper() {
        checkForExistingBody();
        hasStartedReadingBody = true;
        return socketWrapper;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Request request = (Request) o;
        return hasStartedReadingBody == request.hasStartedReadingBody && Objects.equals(headers, request.headers) && Objects.equals(requestLine, request.requestLine) && Objects.equals(body, request.body) && Objects.equals(remoteRequester, request.remoteRequester) && Objects.equals(socketWrapper, request.socketWrapper) && Objects.equals(bodyProcessor, request.bodyProcessor);
    }

    @Override
    public int hashCode() {
        return Objects.hash(headers, requestLine, body, remoteRequester, socketWrapper, bodyProcessor, hasStartedReadingBody);
    }

    @Override
    public String toString() {
        return "Request{" +
                "headers=" + headers +
                ", requestLine=" + requestLine +
                ", body=" + body +
                ", remoteRequester='" + remoteRequester + '\'' +
                ", socketWrapper=" + socketWrapper +
                ", hasStartedReadingBody=" + hasStartedReadingBody +
                '}';
    }

    @Override
    public Iterable<UrlEncodedKeyValue> getUrlEncodedIterable() {
        checkForExistingBody();
        if (!headers.contentType().contains("application/x-www-form-urlencoded")) {
            throw new WebServerException("This request was not sent with a content type of application/x-www-form-urlencoded.  The content type was: " + headers.contentType());
        }
        return bodyProcessor.getUrlEncodedDataIterable(getSocketWrapper().getInputStream(), getHeaders().contentLength());
    }

    /**
     * This method is for verifying that the body is null, and throwing an exception
     * if not.  Several of the methods in this class depend on the InputStream not
     * having been read already - if the body was read, then the InputStream is finished,
     * and any further reading would be incorrect.
     */
    private void checkForExistingBody() {
        if (body != null) {
            throw new WebServerException("Requesting this after getting the body with getBody() will result in incorrect behavior.  " +
                    "If you intend to work with the Request at this level, do not use getBody");
        }
        if (hasStartedReadingBody) {
            throw new WebServerException("The InputStream has begun processing elsewhere.  Results are invalid.");
        }
    }

    @Override
    public Iterable<StreamingMultipartPartition> getMultipartIterable() {
        checkForExistingBody();
        if (!headers.contentType().contains("multipart/form-data")) {
            throw new WebServerException("This request was not sent with a content type of multipart/form-data.  The content type was: " + headers.contentType());
        }
        String boundaryKey = "boundary=";
        String contentType = getHeaders().contentType();
        int indexOfBoundaryKey = contentType.indexOf(boundaryKey);
        String boundaryValue = "";
        if (indexOfBoundaryKey > 0) {
            // grab all the text after the key to obtain the boundary value
            boundaryValue = contentType.substring(indexOfBoundaryKey + boundaryKey.length());
        } else {
            String parsingError = "Did not find a valid boundary value for the multipart input. Returning an empty map and the raw bytes for the body. Header was: " + contentType;
            throw new WebServerException(parsingError);
        }

        if (boundaryValue.isBlank()) {
            String parsingError = "Boundary value was blank. Returning an empty map and the raw bytes for the body. Header was: " + contentType;
            throw new WebServerException(parsingError);
        }

        return bodyProcessor.getMultiPartIterable(getSocketWrapper().getInputStream(), boundaryValue, getHeaders().contentLength());
    }
}