Request.java

package com.renomad.minum.web;

import java.util.Objects;

/**
 * An HTTP request.
 * <p>
 * From <a href="https://en.wikipedia.org/wiki/HTTP#Request_syntax">Wikipedia</a>
 * </p>
 *<p>
 *     A client sends request messages to the server, which consist of:
 *</p>
 * <ul>
 *     <li>
 *      a request line, consisting of the case-sensitive request
 *      method, a space, the requested URL, another space, the
 *      protocol version, a carriage return, and a line feed, e.g.:
 *      <pre>
 *          GET /images/logo.png HTTP/1.1
 *      </pre>
 *      </li>
 *
 *      <li>
 *      zero or more request header fields (at least 1 or more
 *      headers in case of HTTP/1.1), each consisting of the case-insensitive
 *      field name, a colon, optional leading whitespace, the field
 *      value, an optional trailing whitespace and ending with a
 *      carriage return and a line feed, e.g.:
 *
 *      <pre>
 *      Host: www.example.com
 *      Accept-Language: en
 *      </pre>
 *      </li>
 *
 *      <li>
 *      an empty line, consisting of a carriage return and a line feed;
 *      </li>
 *
 *      <li>
 *      an optional message body.
 *      </li>
 *      In the HTTP/1.1 protocol, all header fields except Host: hostname are optional.
 * </ul>
 *
 * <p>
 * A request line containing only the path name is accepted by servers to
 * maintain compatibility with HTTP clients before the HTTP/1.0 specification in RFC 1945.
 *</p>
 *
 */
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());
    }
}