All Downloads are FREE. Search and download functionalities are using the official Maven repository.

objectos.way.HttpExchange Maven / Gradle / Ivy

Go to download

Objectos Way allows you to build full-stack web applications using only Java.

The newest version!
/*
 * Copyright (C) 2016-2023 Objectos Software LTDA.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package objectos.way;

import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.Socket;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Clock;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;

final class HttpExchange implements Http.Exchange, Closeable {

  public enum ParseStatus {
    // keep going
    NORMAL,

    // SocketInput statuses
    EOF,

    UNEXPECTED_EOF,

    OVERFLOW,

    // 400 bad request
    INVALID_METHOD,

    INVALID_TARGET,

    INVALID_PROTOCOL,

    INVALID_REQUEST_LINE_TERMINATOR,

    INVALID_HEADER,

    INVALID_CONTENT_LENGTH,

    // 414 actually
    URI_TOO_LONG;

    public final boolean isError() {
      return this != NORMAL;
    }

    final boolean isNormal() {
      return this == NORMAL;
    }

    final boolean isBadRequest() {
      return compareTo(INVALID_METHOD) >= 0;
    }
  }

  static final class SendException extends RuntimeException {
    private static final long serialVersionUID = 1L;

    public SendException(IOException cause) {
      super(cause);
    }

    @Override
    public final IOException getCause() {
      return (IOException) super.getCause();
    }
  }

  private record Notes(
      Note.Ref2 hexdump
  ) {

    static Notes get() {
      Class s = Http.Exchange.class;

      return new Notes(
          Note.Ref2.create(s, "Hexdump", Note.ERROR)
      );
    }

  }

  private enum RequestBodyKind {
    EMPTY,

    IN_BUFFER,

    FILE;
  }

  private static final Notes NOTES = Notes.get();

  private static final int _START = 0;
  private static final int _PARSE = 1;
  private static final int _REQUEST = 2;
  private static final int _RESPONSE = 3;
  private static final int _PROCESSED = 4;

  private static final int STATE_MASK = 0xF;
  private static final int BITS_MASK = ~STATE_MASK;

  private static final int KEEP_ALIVE = 1 << 4;

  private static final int CONTENT_LENGTH = 1 << 5;

  private static final int CHUNKED = 1 << 6;

  private Map attributes;

  private int bitset;

  private final Clock clock;

  private final Note.Sink noteSink;

  ParseStatus parseStatus;

  private final Socket socket;

  // RequestBody

  private RequestBodyKind requestBodyKind = RequestBodyKind.EMPTY;

  private Path requestBodyDirectory;

  private Path requestBodyFile;

  // RequestHeaders

  Http.HeaderName headerName;

  HttpHeader[] standardHeaders;

  int standardHeadersCount;

  Map unknownHeaders;

  // RequestLine

  private int matcherIndex;

  private Http.Method method;

  private String path;

  private int pathLimit;

  Map pathParams;

  private Map queryParams;

  private boolean queryParamsReady;

  private int queryStart;

  private String rawValue;

  byte versionMajor;

  byte versionMinor;

  // SocketInput

  private static final int HARD_MAX_BUFFER_SIZE = 1 << 14;

  byte[] buffer;

  int bufferIndex;

  int bufferLimit;

  private final InputStream inputStream;

  int lineLimit;

  private final int maxBufferSize;

  public HttpExchange(Socket socket, int bufferSizeInitial, int bufferSizeMax, Clock clock, Note.Sink noteSink) throws IOException {
    this(socket, socket.getInputStream(), bufferSizeInitial, bufferSizeMax, clock, noteSink);
  }

  private HttpExchange(Socket socket, InputStream inputStream, int bufferSizeInitial, int bufferSizeMax, Clock clock, Note.Sink noteSink) {
    this.socket = socket;

    bufferLimit = powerOfTwo(bufferSizeInitial);

    buffer = new byte[bufferLimit];

    this.maxBufferSize = powerOfTwo(bufferSizeMax);

    this.clock = clock;

    this.inputStream = inputStream;

    this.noteSink = noteSink;

    bufferLimit = 0;

    bufferIndex = 0;

    lineLimit = 0;

    parseStatus = ParseStatus.NORMAL;

    setState(_START);
  }

  /**
   * Parses the specified string into a new request-target instance.
   *
   * @param target
   *        the raw (undecoded) request-target value
   *
   * @return a new request target instance
   *
   * @throws IllegalArgumentException
   *         if the string represents an invalid request-target value
   */
  public static HttpExchange parseRequestTarget(String target) {
    Objects.requireNonNull(target, "target == null");

    // append a line terminator
    target = target + " \r\n";

    byte[] bytes;
    bytes = target.getBytes(StandardCharsets.UTF_8);

    // in-memory stream... no closing needed...
    InputStream inputStream;
    inputStream = new ByteArrayInputStream(bytes);

    HttpExchange requestLine;
    requestLine = new HttpExchange(null, inputStream, 1024, 4096, null, null);

    try {
      requestLine.parseLine();

      requestLine.parseRequestTarget();
    } catch (IOException e) {
      throw new AssertionError("In-memory stream does not throw IOException", e);
    }

    ParseStatus parseStatus;
    parseStatus = requestLine.parseStatus;

    if (parseStatus.isError()) {
      throw new IllegalArgumentException(parseStatus.name());
    }

    return requestLine;
  }

  static final int powerOfTwo(int size) {
    // maybe size is already power of 2
    int x;
    x = size - 1;

    int leading;
    leading = Integer.numberOfLeadingZeros(x);

    int n;
    n = -1 >>> leading;

    if (n < 0) {
      // should not happen as minimal buffer size is 128
      throw new IllegalArgumentException("Buffer size is too small");
    }

    if (n >= HARD_MAX_BUFFER_SIZE) {
      return HARD_MAX_BUFFER_SIZE;
    }

    return n + 1;
  }

  @Override
  public final void close() throws IOException {
    try {
      requestBodyClose();
    } finally {
      socket.close();
    }
  }

  // ##################################################################
  // # BEGIN: HTTP/1.1 request parsing
  // ##################################################################

  private static final byte[] CLOSE_BYTES = Http.utf8("close");

  private static final byte[] KEEP_ALIVE_BYTES = Http.utf8("keep-alive");

  public final ParseStatus parse() throws IOException, IllegalStateException {
    if (testState(_START)) {
      // noop
    }

    else if (testState(_PROCESSED)) {
      resetSocketInput();

      resetRequestLine();

      resetHeaders();

      resetRequestBody();

      resetServerLoop();
    }

    else {
      throw new IllegalStateException("""
      The parse() metod must only be called after:
      1) loop creation; or
      2) a successful commit operation
      """);
    }

    // force bits reset
    bitset = _PARSE;

    // request line

    parseRequestLine();

    if (parseStatus.isError()) {
      return parseStatus;
    }

    // request headers

    parseHeaders();

    if (parseStatus.isError()) {
      return parseStatus;
    }

    // request body

    parseRequestBody();

    if (parseStatus.isError()) {
      return parseStatus;
    }

    parseRequestEnd();

    return parseStatus;
  }

  private void resetSocketInput() {
    bufferLimit = 0;

    bufferIndex = 0;

    lineLimit = 0;

    parseStatus = ParseStatus.NORMAL;
  }

  private void resetRequestLine() {
    method = null;

    pathLimit = 0;

    if (pathParams != null) {
      pathParams.clear();
    }

    path = null;

    if (queryParams != null) {
      queryParams.clear();
    }

    queryParamsReady = false;

    queryStart = 0;

    rawValue = null;

    versionMajor = versionMinor = 0;
  }

  private void resetHeaders() {
    headerName = null;

    if (standardHeaders != null) {
      Arrays.fill(standardHeaders, null);
    }

    standardHeadersCount = 0;

    if (unknownHeaders != null) {
      unknownHeaders.clear();
    }
  }

  private void resetRequestBody() {
    requestBodyKind = RequestBodyKind.EMPTY;

    requestBodyFile = null;
  }

  private void resetServerLoop() {
    if (attributes != null) {
      attributes.clear();
    }
  }

  // ##################################################################
  // # BEGIN: HTTP/1.1 request parsing || request line
  // ##################################################################

  final void parseRequestLine() throws IOException {
    parseLine();

    if (parseStatus == ParseStatus.UNEXPECTED_EOF) {
      if (bufferLimit == 0) {
        // buffer is empty, this is an expected EOF
        parseStatus = ParseStatus.EOF;
      }

      return;
    }

    parseMethod();

    if (method == null) {
      // parse method failed -> bad request
      parseStatus = ParseStatus.INVALID_METHOD;

      return;
    }

    parseRequestTarget();

    parseVersion();

    if (parseStatus.isError()) {
      // bad request -> fail
      return;
    }

    if (!consumeIfEndOfLine()) {
      parseStatus = ParseStatus.INVALID_REQUEST_LINE_TERMINATOR;

      return;
    }
  }

  private static final byte[] _CONNECT = "CONNECT ".getBytes(StandardCharsets.UTF_8);

  private static final byte[] _DELETE = "DELETE ".getBytes(StandardCharsets.UTF_8);

  private static final byte[] _GET = "GET ".getBytes(StandardCharsets.UTF_8);

  private static final byte[] _HEAD = "HEAD ".getBytes(StandardCharsets.UTF_8);

  private static final byte[] _OPTIONS = "OPTIONS ".getBytes(StandardCharsets.UTF_8);

  private static final byte[] _POST = "POST ".getBytes(StandardCharsets.UTF_8);

  private static final byte[] _PUT = "PUT ".getBytes(StandardCharsets.UTF_8);

  private static final byte[] _PATCH = "PATCH ".getBytes(StandardCharsets.UTF_8);

  private static final byte[] _TRACE = "TRACE ".getBytes(StandardCharsets.UTF_8);

  private void parseMethod() throws IOException {
    if (bufferIndex >= lineLimit) {
      // empty line... nothing to do
      return;
    }

    byte first;
    first = buffer[bufferIndex];

    // based on the first char, we select out method candidate

    switch (first) {
      case 'C' -> parseMethod0(Http.Method.CONNECT, _CONNECT);

      case 'D' -> parseMethod0(Http.Method.DELETE, _DELETE);

      case 'G' -> parseMethod0(Http.Method.GET, _GET);

      case 'H' -> parseMethod0(Http.Method.HEAD, _HEAD);

      case 'O' -> parseMethod0(Http.Method.OPTIONS, _OPTIONS);

      case 'P' -> parseMethodP();

      case 'T' -> parseMethod0(Http.Method.TRACE, _TRACE);
    }
  }

  private void parseMethod0(Http.Method candidate, byte[] candidateBytes) throws IOException {
    if (matches(candidateBytes)) {
      method = candidate;
    }
  }

  private void parseMethodP() throws IOException {
    // method starts with a P. It might be:
    // - POST
    // - PUT
    // - PATCH

    // we'll try them in sequence

    parseMethod0(Http.Method.POST, _POST);

    if (method != null) {
      return;
    }

    parseMethod0(Http.Method.PUT, _PUT);

    if (method != null) {
      return;
    }

    parseMethod0(Http.Method.PATCH, _PATCH);

    if (method != null) {
      return;
    }
  }

  final void parseRequestTarget() throws IOException {
    int startIndex;
    startIndex = parsePathStart();

    if (parseStatus.isError()) {
      // bad request -> fail
      return;
    }

    parsePathRest(startIndex);

    if (parseStatus.isError()) {
      // bad request -> fail
      return;
    }
  }

  private int parsePathStart() throws IOException {
    // we will check if the request target starts with a '/' char

    int targetStart;
    targetStart = bufferIndex;

    if (bufferIndex >= lineLimit) {
      // reached EOL -> bad request
      parseStatus = ParseStatus.INVALID_TARGET;

      return 0;
    }

    byte b;
    b = buffer[bufferIndex++];

    if (b != Bytes.SOLIDUS) {
      // first char IS NOT '/' => BAD_REQUEST
      parseStatus = ParseStatus.INVALID_TARGET;

      return 0;
    }

    // mark request path start

    return targetStart;
  }

  private void parsePathRest(int startIndex) throws IOException {
    // we will look for the first:
    // - ? char
    // - SP char
    int index;
    index = indexOf(Bytes.QUESTION_MARK, Bytes.SP);

    if (index < 0) {
      // trailing char was not found
      parseStatus = ParseStatus.URI_TOO_LONG;

      return;
    }

    // index where path ends
    int pathEndIndex;
    pathEndIndex = index;

    // as of now target ends at the path
    int targetEndIndex;
    targetEndIndex = pathEndIndex;

    // as of now query starts and ends at path i.e. len = 0
    int queryStartIndex;
    queryStartIndex = pathEndIndex;

    // we'll continue at the '?' or SP char
    bufferIndex = index;

    byte b;
    b = buffer[bufferIndex++];

    if (b == Bytes.QUESTION_MARK) {
      queryStartIndex = bufferIndex;

      targetEndIndex = indexOf(Bytes.SP);

      if (targetEndIndex < 0) {
        // trailing char was not found
        parseStatus = ParseStatus.URI_TOO_LONG;

        return;
      }

      // we'll continue immediately after the SP
      bufferIndex = targetEndIndex + 1;
    }

    rawValue = bufferToString(startIndex, targetEndIndex);

    pathLimit = pathEndIndex - startIndex;

    queryStart = queryStartIndex - startIndex;
  }

  static final byte[] HTTP_VERSION_PREFIX = {'H', 'T', 'T', 'P', '/'};

  private void parseVersion() {
    // 'H' 'T' 'T' 'P' '/' '1' '.' '1' = 8 bytes

    if (!matches(HTTP_VERSION_PREFIX)) {
      // buffer does not start with 'HTTP/'
      parseStatus = ParseStatus.INVALID_PROTOCOL;

      return;
    }

    // check if we  have '1' '.' '1' = 3 bytes

    int requiredIndex;
    requiredIndex = bufferIndex + 3 - 1;

    if (requiredIndex >= lineLimit) {
      parseStatus = ParseStatus.INVALID_PROTOCOL;

      return;
    }

    byte maybeMajor;
    maybeMajor = buffer[bufferIndex++];

    if (!Http.isDigit(maybeMajor)) {
      // major version is not a digit => bad request
      parseStatus = ParseStatus.INVALID_PROTOCOL;

      return;
    }

    byte maybeDot;
    maybeDot = buffer[bufferIndex++];

    if (maybeDot != '.') {
      // major version not followed by a DOT => bad request
      parseStatus = ParseStatus.INVALID_PROTOCOL;

      return;
    }

    byte maybeMinor;
    maybeMinor = buffer[bufferIndex++];

    if (!Http.isDigit(maybeMinor)) {
      // minor version is not a digit => bad request
      parseStatus = ParseStatus.INVALID_PROTOCOL;

      return;
    }

    versionMajor = (byte) (maybeMajor - 0x30);

    versionMinor = (byte) (maybeMinor - 0x30);
  }

  // ##################################################################
  // # END: HTTP/1.1 request parsing || request line
  // ##################################################################

  // ##################################################################
  // # BEGIN: HTTP/1.1 request parsing || headers
  // ##################################################################

  final void parseHeaders() throws IOException {
    parseLine();

    while (parseStatus.isNormal() && !consumeIfEmptyLine()) {
      parseStandardHeaderName();

      if (parseStatus.isError()) {
        break;
      }

      if (headerName == null) {
        parseUnknownHeaderName();

        if (parseStatus.isError()) {
          break;
        }
      }

      parseHeaderValue();

      parseLine();
    }

    // clear last header name just in case
    headerName = null;
  }

  private void parseStandardHeaderName() {
    // we reset any previous found header name

    headerName = null;

    // we will use the first char as hash code
    if (bufferIndex >= lineLimit) {
      parseStatus = ParseStatus.INVALID_HEADER;

      return;
    }

    final byte first;
    first = buffer[bufferIndex];

    // ad hoc hash map

    switch (first) {
      case 'A' -> parseHeaderName0(
          Http.HeaderName.ACCEPT_ENCODING
      );

      case 'C' -> parseHeaderName0(
          Http.HeaderName.CONNECTION,
          Http.HeaderName.CONTENT_LENGTH,
          Http.HeaderName.CONTENT_TYPE,
          Http.HeaderName.COOKIE
      );

      case 'D' -> parseHeaderName0(
          Http.HeaderName.DATE
      );

      case 'F' -> parseHeaderName0(
          Http.HeaderName.FROM
      );

      case 'H' -> parseHeaderName0(
          Http.HeaderName.HOST
      );

      case 'T' -> parseHeaderName0(
          Http.HeaderName.TRANSFER_ENCODING
      );

      case 'U' -> parseHeaderName0(
          Http.HeaderName.USER_AGENT
      );
    }
  }

  static final byte[][] STD_HEADER_NAME_BYTES;

  static {
    int size;
    size = Http.headerNameSize();

    byte[][] map;
    map = new byte[size][];

    for (int i = 0; i < size; i++) {
      HttpHeaderName headerName;
      headerName = HttpHeaderName.standardName(i);

      String name;
      name = headerName.capitalized();

      map[i] = name.getBytes(StandardCharsets.UTF_8);
    }

    STD_HEADER_NAME_BYTES = map;
  }

  private void parseHeaderName0(Http.HeaderName candidate) {
    int index;
    index = candidate.index();

    final byte[] candidateBytes;
    candidateBytes = STD_HEADER_NAME_BYTES[index];

    if (!matches(candidateBytes)) {
      // does not match -> try next

      return;
    }

    if (bufferIndex >= lineLimit) {
      // matches but reached end of line -> bad request

      parseStatus = ParseStatus.INVALID_HEADER;

      return;
    }

    byte maybeColon;
    maybeColon = buffer[bufferIndex++];

    if (maybeColon != Bytes.COLON) {
      // matches but is not followed by a colon character

      parseStatus = ParseStatus.INVALID_HEADER;

      return;
    }

    headerName = candidate;
  }

  private void parseHeaderName0(Http.HeaderName c0, Http.HeaderName c1, Http.HeaderName c2,
      Http.HeaderName c3) {
    parseHeaderName0(c0);

    if (headerName != null) {
      return;
    }

    parseHeaderName0(c1);

    if (headerName != null) {
      return;
    }

    parseHeaderName0(c2);

    if (headerName != null) {
      return;
    }

    parseHeaderName0(c3);
  }

  private void parseUnknownHeaderName() {
    int startIndex;
    startIndex = bufferIndex;

    int colonIndex;
    colonIndex = indexOf(Bytes.COLON);

    if (colonIndex < 0) {
      // no colon found
      parseStatus = ParseStatus.INVALID_HEADER;

      return;
    }

    if (startIndex == colonIndex) {
      // empty header name
      parseStatus = ParseStatus.INVALID_HEADER;

      return;
    }

    String name;
    name = bufferToString(startIndex, colonIndex);

    headerName = Http.HeaderName.create(name);

    // resume immediately after the colon
    bufferIndex = colonIndex + 1;
  }

  private void parseHeaderValue() {
    int startIndex;
    startIndex = parseHeaderValueStart();

    int endIndex;
    endIndex = parseHeaderValueEnd(startIndex);

    if (startIndex > endIndex) {
      // value has negative length... is it possible?
      hexDump();

      throw new UnsupportedOperationException("Implement me");
    }

    int index;
    index = headerName.index();

    if (index >= 0) {
      if (standardHeaders == null) {
        int size;
        size = HttpHeaderName.standardNamesSize();

        standardHeaders = new HttpHeader[size];
      }

      HttpHeader header;
      header = standardHeaders[index];

      if (header == null) {
        header = new HttpHeader(headerName, this, startIndex, endIndex);

        standardHeadersCount++;
      } else {
        header = header.add(startIndex, endIndex);
      }

      standardHeaders[index] = header;
    } else {
      if (unknownHeaders == null) {
        unknownHeaders = new HashMap<>();
      }

      Http.HeaderName name;
      name = headerName;

      HttpHeader header;
      header = unknownHeaders.get(name);

      if (header == null) {
        header = new HttpHeader(headerName, this, startIndex, endIndex);
      } else {
        header = header.add(startIndex, endIndex);
      }

      unknownHeaders.put(name, header);
    }
  }

  final void hexDump() {
    HexFormat format;
    format = HexFormat.of();

    String bufferDump;
    bufferDump = format.formatHex(buffer, 0, bufferLimit);

    String args;
    args = "bufferIndex=" + bufferIndex + ";lineLimit=" + lineLimit;

    noteSink.send(NOTES.hexdump, bufferDump, args);
  }

  private int parseHeaderValueStart() {
    // consumes and discard a single leading OWS if present
    byte maybeOws;
    maybeOws = buffer[bufferIndex];

    if (Bytes.isOptionalWhitespace(maybeOws)) {
      // consume and discard leading OWS
      bufferIndex++;
    }

    return bufferIndex;
  }

  private int parseHeaderValueEnd(int startIndex) {
    int end;
    end = lineLimit;

    byte maybeCR;
    maybeCR = buffer[end - 1];

    if (maybeCR == Bytes.CR) {
      // value ends at the CR of the line end CRLF
      end = end - 1;
    }

    if (end != startIndex) {

      byte maybeOWS;
      maybeOWS = buffer[end - 1];

      if (Bytes.isOptionalWhitespace(maybeOWS)) {
        // value ends at the trailing OWS
        end = end - 1;
      }

    }

    // resume immediately after lineLimite
    bufferIndex = lineLimit + 1;

    return end;
  }

  // ##################################################################
  // # END: HTTP/1.1 request parsing || headers
  // ##################################################################

  // ##################################################################
  // # BEGIN: HTTP/1.1 request parsing || body
  // ##################################################################

  final void parseRequestBody() throws IOException {
    HttpHeader contentLength;
    contentLength = headerUnchecked(Http.HeaderName.CONTENT_LENGTH);

    if (contentLength != null) {
      long value;
      value = contentLength.unsignedLongValue();

      if (value < 0) {
        parseStatus = ParseStatus.INVALID_HEADER;
      }

      else if (canBuffer(value)) {
        int read;
        read = read(value);

        if (read < 0) {
          throw new EOFException();
        }

        requestBodyKind = RequestBodyKind.IN_BUFFER;
      }

      else {
        if (requestBodyDirectory == null) {
          requestBodyFile = Files.createTempFile("objectos-way-request-body-", ".tmp");
        } else {
          requestBodyFile = Files.createTempFile(requestBodyDirectory, "objectos-way-request-body-", ".tmp");
        }

        long read;
        read = read(requestBodyFile, value);

        if (read < 0) {
          parseStatus = ParseStatus.EOF;
        } else {
          requestBodyKind = RequestBodyKind.FILE;
        }
      }

      return;
    }

    HttpHeader transferEncoding;
    transferEncoding = headerUnchecked(Http.HeaderName.TRANSFER_ENCODING);

    if (transferEncoding != null) {
      throw new UnsupportedOperationException("Implement me");
    }
  }

  // ##################################################################
  // # END: HTTP/1.1 request parsing || body
  // ##################################################################

  // ##################################################################
  // # BEGIN: HTTP/1.1 request parsing || keep alive
  // ##################################################################

  final void parseRequestEnd() {
    // handle keep alive

    clearBit(KEEP_ALIVE);

    if (versionMajor == 1 && versionMinor == 1) {
      setBit(KEEP_ALIVE);
    }

    HttpHeader connection;
    connection = headerUnchecked(Http.HeaderName.CONNECTION);

    if (connection != null) {
      if (connection.contentEquals(KEEP_ALIVE_BYTES)) {
        setBit(KEEP_ALIVE);
      }

      else if (connection.contentEquals(CLOSE_BYTES)) {
        clearBit(KEEP_ALIVE);
      }
    }

    setState(_REQUEST);
  }

  // ##################################################################
  // # END: HTTP/1.1 request parsing || keep alive
  // ##################################################################

  private void checkRequest() {
    Check.state(
        !badRequest(),

        """
        This request method can only be invoked:
        - after a successful parse() operation; and
        - before any response related method invocation.
        """
    );
  }

  private boolean badRequest() {
    Check.state(testState(_REQUEST), "Http.Request.Method can only be invoked after a parse() operation");

    return parseStatus.isBadRequest();
  }

  // ##################################################################
  // # BEGIN: Http.Exchange API || request line
  // ##################################################################

  @Override
  public final Http.Method method() {
    return method;
  }

  // ##################################################################
  // # END: Http.Exchange API || request line
  // ##################################################################

  // ##################################################################
  // # BEGIN: Http.Exchange API || request target
  // ##################################################################

  @Override
  public final String path() {
    if (path == null) {
      String raw;
      raw = rawPath();

      path = decode(raw);
    }

    return path;
  }

  @Override
  public final String pathParam(String name) {
    Check.notNull(name, "name == null");

    String result;
    result = null;

    if (pathParams != null) {
      result = pathParams.get(name);
    }

    return result;
  }

  @Override
  public final String queryParam(String name) {
    Check.notNull(name, "name == null");

    Map params;
    params = $queryParams();

    Object maybe;
    maybe = params.get(name);

    return switch (maybe) {
      case null -> null;

      case String s -> s;

      case List l -> (String) l.get(0);

      default -> throw new AssertionError(
          "Type should not have been put into the map: " + maybe.getClass()
      );
    };
  }

  @Override
  public final Set queryParamNames() {
    Map params;
    params = $queryParams();

    return params.keySet();
  }

  private Map $queryParams() {
    if (!queryParamsReady) {
      if (queryParams == null) {
        queryParams = Util.createMap();
      }

      makeQueryParams(queryParams, this::decode);

      queryParamsReady = true;
    }

    return queryParams;
  }

  @Override
  public final String rawPath() {
    return rawValue.substring(0, pathLimit);
  }

  @Override
  public final String rawQuery() {
    return queryStart == pathLimit ? null : rawValue.substring(queryStart);
  }

  private Map $rawQueryParams() {
    Map map;
    map = Util.createMap();

    makeQueryParams(map, Function.identity());

    return map;
  }

  public final String rawValue() {
    return rawValue;
  }

  public final String rawValue(String queryParamName, String queryParamValue) {
    Check.notNull(queryParamName, "queryParamName == null");
    Check.notNull(queryParamValue, "queryParamValue == null");

    StringBuilder builder;
    builder = new StringBuilder(rawPath());

    builder.append('?');

    Map params;
    params = $rawQueryParams();

    String rawName;
    rawName = encode(queryParamName);

    String rawValue;
    rawValue = encode(queryParamValue);

    params.put(rawName, rawValue);

    int count;
    count = 0;

    for (String key : params.keySet()) {
      if (count++ > 0) {
        builder.append('&');
      }

      builder.append(key);

      builder.append('=');

      Object value;
      value = params.get(key);

      if (value instanceof String s) {
        builder.append(s);
      }

      else {
        throw new UnsupportedOperationException("Implement me");
      }
    }

    return builder.toString();
  }

  private void makeQueryParams(Map map, Function decoder) {
    int queryLength;
    queryLength = rawValue.length() - queryStart;

    if (queryLength < 2) {
      // query is empty: either "" or "?"
      return;
    }

    String source;
    source = rawQuery();

    StringBuilder sb;
    sb = new StringBuilder();

    String key;
    key = null;

    for (int i = 0, len = source.length(); i < len; i++) {
      char c;
      c = source.charAt(i);

      switch (c) {
        case '=' -> {
          key = sb.toString();

          sb.setLength(0);

          putQueryParams(map, decoder, key, "");
        }

        case '&' -> {
          String value;
          value = sb.toString();

          sb.setLength(0);

          if (key == null) {
            putQueryParams(map, decoder, value, "");

            continue;
          }

          putQueryParams(map, decoder, key, value);

          key = null;
        }

        default -> sb.append(c);
      }
    }

    String value;
    value = sb.toString();

    if (key != null) {
      putQueryParams(map, decoder, key, value);
    } else {
      putQueryParams(map, decoder, value, "");
    }
  }

  @SuppressWarnings("unchecked")
  private void putQueryParams(Map map, Function decoder, String rawKey, String rawValue) {
    String key;
    key = decoder.apply(rawKey);

    String value;
    value = decoder.apply(rawValue);

    Object oldValue;
    oldValue = map.put(key, value);

    if (oldValue == null) {
      return;
    }

    if (oldValue.equals("")) {
      return;
    }

    if (oldValue instanceof String s) {

      if (value.equals("")) {
        map.put(key, s);
      } else {
        List list;
        list = Util.createList();

        list.add(s);

        list.add(value);

        map.put(key, list);
      }

    }

    else {
      List list;
      list = (List) oldValue;

      list.add(value);

      map.put(key, list);
    }
  }

  // ##################################################################
  // # END: Http.Exchange API || request target
  // ##################################################################

  // ##################################################################
  // # BEGIN: Http.Exchange API || request headers
  // ##################################################################

  @Override
  public final String header(Http.HeaderName name) {
    Check.notNull(name, "name == null");

    int index;
    index = name.index();

    if (index >= 0) {

      if (standardHeaders == null) {
        return null;
      }

      HttpHeader maybe;
      maybe = standardHeaders[index];

      if (maybe != null) {
        return maybe.get();
      } else {
        return null;
      }

    } else {

      if (unknownHeaders == null) {
        return null;
      }

      HttpHeader maybe;
      maybe = unknownHeaders.get(name);

      if (maybe != null) {
        return maybe.get();
      } else {
        return null;
      }

    }
  }

  public final int size() {
    int size = 0;

    if (standardHeaders != null) {
      size += standardHeadersCount;
    }

    if (unknownHeaders != null) {
      size += unknownHeaders.size();
    }

    return size;
  }

  final HttpHeader headerUnchecked(Http.HeaderName name) {
    if (standardHeaders == null) {
      return null;
    } else {
      int index;
      index = name.index();

      return standardHeaders[index];
    }
  }

  // ##################################################################
  // # END: Http.Exchange API || request headers
  // ##################################################################

  // ##################################################################
  // # BEGIN: Http.Exchange API || request body
  // ##################################################################

  public void requestBodyDirectory(java.nio.file.Path directory) {
    requestBodyDirectory = directory;
  }

  private void requestBodyClose() throws IOException {
    if (requestBodyFile != null) {
      Files.delete(requestBodyFile);
    }
  }

  @Override
  public final InputStream bodyInputStream() throws IOException {
    checkRequest();

    return switch (requestBodyKind) {
      case EMPTY -> InputStream.nullInputStream();

      case IN_BUFFER -> openStreamImpl();

      case FILE -> Files.newInputStream(requestBodyFile);
    };
  }

  private InputStream openStreamImpl() {
    int length;
    length = bufferLimit - bufferIndex;

    return new ByteArrayInputStream(buffer, bufferIndex, length);
  }

  // ##################################################################
  // # END: Http.Exchange API || request body
  // ##################################################################

  // ##################################################################
  // # BEGIN: Http.Exchange API || request attributes
  // ##################################################################

  @Override
  public final  void set(Class key, T value) {
    String name;
    name = key.getName(); // implicit null check

    Check.notNull(value, "value == null");

    Map map;
    map = attributes();

    map.put(name, value);
  }

  @SuppressWarnings("unchecked")
  @Override
  public final  T get(Class key) {
    String name;
    name = key.getName(); // implicit null check

    Map map;
    map = attributes();

    return (T) map.get(name);
  }

  private Map attributes() {
    if (attributes == null) {
      attributes = Util.createMap();
    }

    return attributes;
  }

  // ##################################################################
  // # END: Http.Exchange API || request attributes
  // ##################################################################

  // ##################################################################
  // # BEGIN: Http.Exchange API || response
  // ##################################################################

  private void checkResponse() {
    if (testState(_REQUEST)) {
      bufferIndex = 0;

      setState(_RESPONSE);

      return;
    }

    if (testState(_RESPONSE)) {
      return;
    }

    throw new IllegalStateException(
        """
        Response methods can only be invoked:
        - after a successful parse() operation; and
        - before the commit() method invocation.
        """
    );
  }

  static final byte[][] STATUS_LINES;

  static {
    int size;
    size = HttpStatus.size();

    byte[][] map;
    map = new byte[size][];

    for (int index = 0; index < size; index++) {
      HttpStatus status;
      status = HttpStatus.get(index);

      String response;
      response = Integer.toString(status.code()) + " " + status.reasonPhrase() + "\r\n";

      map[index] = Http.utf8(response);
    }

    STATUS_LINES = map;
  }

  @Override
  public final void status(Http.Status status) {
    checkResponse();

    Http.Version version;
    version = Http.Version.HTTP_1_1;

    writeBytes(version.responseBytes);

    HttpStatus internal;
    internal = (HttpStatus) status;

    byte[] statusBytes;
    statusBytes = STATUS_LINES[internal.index];

    writeBytes(statusBytes);
  }

  @Override
  public final void header(Http.HeaderName name, long value) {
    checkResponse();
    Check.notNull(name, "name == null");

    header0(name, Long.toString(value));
  }

  @Override
  public final void header(Http.HeaderName name, String value) {
    checkResponse();
    Check.notNull(name, "name == null");
    Check.notNull(value, "value == null");

    header0(name, value);
  }

  @Override
  public final void dateNow() {
    checkResponse();

    Clock theClock;
    theClock = clock;

    if (theClock == null) {
      theClock = Clock.systemUTC();
    }

    ZonedDateTime now;
    now = ZonedDateTime.now(theClock);

    String value;
    value = Http.formatDate(now);

    header0(Http.HeaderName.DATE, value);
  }

  private void header0(Http.HeaderName name, String value) { // write our the name
    int index;
    index = name.index();

    byte[] nameBytes;

    if (index >= 0) {
      nameBytes = STD_HEADER_NAME_BYTES[index];
    } else {
      String capitalized;
      capitalized = name.capitalized();

      nameBytes = capitalized.getBytes(StandardCharsets.UTF_8);
    }

    writeBytes(nameBytes);

    // write out the separator
    writeBytes(Bytes.COLONSP);

    // write out the value
    byte[] valueBytes;
    valueBytes = value.getBytes(StandardCharsets.UTF_8);

    writeBytes(valueBytes);

    writeBytes(Bytes.CRLF);

    // handle connection: close if necessary
    if (name == Http.HeaderName.CONNECTION && value.equalsIgnoreCase("close")) {
      clearBit(KEEP_ALIVE);
    }

    else if (name == Http.HeaderName.CONTENT_LENGTH) {
      setBit(CONTENT_LENGTH);
    }

    else if (name == Http.HeaderName.TRANSFER_ENCODING && value.toLowerCase().contains("chunked")) {
      setBit(CHUNKED);
    }
  }

  @Override
  public final void send() {
    checkResponse();

    try {
      sendStart();
    } catch (IOException e) {
      throw new SendException(e);
    } finally {
      setState(_PROCESSED);
    }
  }

  @Override
  public final void send(byte[] body) {
    if (method == Http.Method.HEAD) {

      send();

    } else {

      checkResponse();

      try {
        OutputStream outputStream;
        outputStream = sendStart();

        outputStream.write(body, 0, body.length); // implicity body null-check
      } catch (IOException e) {
        throw new SendException(e);
      } finally {
        setState(_PROCESSED);
      }

    }
  }

  private static final byte[] CHUNKED_TRAILER = "0\r\n\r\n".getBytes(StandardCharsets.UTF_8);

  public final void send(Lang.CharWritable body, Charset charset) {
    Objects.requireNonNull(body, "body == null");
    Objects.requireNonNull(charset, "charset == null");

    if (testBit(CONTENT_LENGTH)) {
      throw new IllegalStateException(
          "Content-Length must not be set with a CharWritable body"
      );
    }

    if (!testBit(CHUNKED)) {
      throw new IllegalStateException(
          "Transfer-Encoding: chunked must be set with a CharWritable body"
      );
    }

    if (method == Http.Method.HEAD) {

      send();

    } else {

      checkResponse();

      try {
        OutputStream outputStream;
        outputStream = sendStart();

        bufferIndex = 0;

        CharWritableAppendable out;
        out = new CharWritableAppendable(outputStream, charset);

        body.writeTo(out);

        out.flush();

        outputStream.write(CHUNKED_TRAILER);
      } catch (IOException e) {
        throw new SendException(e);
      } finally {
        setState(_PROCESSED);
      }

    }
  }

  @Override
  public final void send(java.nio.file.Path file) {
    Objects.requireNonNull(file, "file == null");

    if (method == Http.Method.HEAD) {

      send();

    } else {

      checkResponse();

      try {
        OutputStream outputStream;
        outputStream = sendStart();

        try (InputStream in = Files.newInputStream(file)) {
          in.transferTo(outputStream);
        }
      } catch (IOException e) {
        throw new SendException(e);
      } finally {
        setState(_PROCESSED);
      }

    }
  }

  private OutputStream sendStart() throws IOException {
    writeBytes(Bytes.CRLF);

    OutputStream outputStream;
    outputStream = socket.getOutputStream();

    outputStream.write(buffer, 0, bufferIndex);

    return outputStream;
  }

  private class CharWritableAppendable implements Appendable {
    private final OutputStream outputStream;

    private final Charset charset;

    public CharWritableAppendable(OutputStream outputStream, Charset charset) {
      this.outputStream = outputStream;

      this.charset = charset;
    }

    @Override
    public Appendable append(char c) throws IOException {
      String s;
      s = Character.toString(c);

      return append(s);
    }

    @Override
    public Appendable append(CharSequence csq) throws IOException {
      String s;
      s = csq.toString();

      byte[] bytes;
      bytes = s.getBytes(charset);

      buffer(bytes);

      return this;
    }

    @Override
    public Appendable append(CharSequence csq, int start, int end) throws IOException {
      CharSequence sub;
      sub = csq.subSequence(start, end);

      return append(sub);
    }

    private void buffer(byte[] bytes) throws IOException {
      int bytesIndex;
      bytesIndex = 0;

      int remaining;
      remaining = bytes.length - bytesIndex;

      while (remaining > 0) {
        int maxAvailable;
        maxAvailable = maxBufferSize - bufferIndex;

        if (maxAvailable == 0) {
          flush();

          maxAvailable = maxBufferSize - bufferIndex;
        }

        int bytesToCopy;
        bytesToCopy = Math.min(remaining, maxAvailable);

        int requiredIndex;
        requiredIndex = bufferIndex + bytesToCopy - 1;

        if (requiredIndex >= buffer.length) {
          int minSize;
          minSize = requiredIndex + 1;

          int newSize;
          newSize = powerOfTwo(minSize);

          if (newSize > maxBufferSize) {
            throw new UnsupportedOperationException("Implement me");
          }

          buffer = Arrays.copyOf(buffer, newSize);
        }

        System.arraycopy(bytes, bytesIndex, buffer, bufferIndex, bytesToCopy);

        bufferIndex += bytesToCopy;

        bytesIndex += bytesToCopy;

        remaining -= bytesToCopy;
      }
    }

    final void flush() throws IOException {
      int chunkLength;
      chunkLength = bufferIndex;

      String lengthDigits;
      lengthDigits = Integer.toHexString(chunkLength);

      byte[] lengthBytes;
      lengthBytes = (lengthDigits + "\r\n").getBytes(StandardCharsets.UTF_8);

      outputStream.write(lengthBytes, 0, lengthBytes.length);

      int bufferRemaining;
      bufferRemaining = buffer.length - bufferIndex;

      if (bufferRemaining >= 2) {
        buffer[bufferIndex++] = Bytes.CR;
        buffer[bufferIndex++] = Bytes.LF;

        outputStream.write(buffer, 0, bufferIndex);
      } else {
        outputStream.write(buffer, 0, bufferIndex);

        outputStream.write(Bytes.CRLF);
      }

      bufferIndex = 0;
    }
  }

  // 200 OK

  @Override
  public final void ok() {
    status(Http.Status.OK);

    dateNow();

    send();
  }

  @Override
  public final void ok(Lang.MediaObject object) {
    String contentType;
    contentType = object.contentType();

    if (contentType == null) {
      throw new NullPointerException("Provided Lang.MediaObject provided a null content-type");
    }

    byte[] bytes;
    bytes = object.mediaBytes();

    if (bytes == null) {
      throw new NullPointerException("Provided Lang.MediaObject provided a null byte array");
    }

    status(Http.Status.OK);

    dateNow();

    header(Http.HeaderName.CONTENT_TYPE, contentType);

    header(Http.HeaderName.CONTENT_LENGTH, bytes.length);

    send(bytes);
  }

  // 404 NOT FOUND

  @Override
  public final void notFound() {
    status(Http.Status.NOT_FOUND);

    dateNow();

    header0(Http.HeaderName.CONNECTION, "close");

    send();
  }

  // 405 METHOD NOT ALLOWED

  @Override
  public final void methodNotAllowed() {
    status(Http.Status.METHOD_NOT_ALLOWED);

    dateNow();

    header0(Http.HeaderName.CONNECTION, "close");

    send();
  }

  // 500 INTERNAL SERVER ERROR

  @Override
  public final void internalServerError(Throwable t) {
    StringWriter sw;
    sw = new StringWriter();

    PrintWriter pw;
    pw = new PrintWriter(sw);

    t.printStackTrace(pw);

    String msg;
    msg = sw.toString();

    byte[] bytes;
    bytes = msg.getBytes();

    status(Http.Status.INTERNAL_SERVER_ERROR);

    dateNow();

    header(Http.HeaderName.CONTENT_LENGTH, bytes.length);

    header(Http.HeaderName.CONTENT_TYPE, "text/plain");

    header(Http.HeaderName.CONNECTION, "close");

    send(bytes);
  }

  private void writeBytes(byte[] bytes) {
    int length;
    length = bytes.length;

    int requiredIndex;
    requiredIndex = bufferIndex + length - 1;

    if (requiredIndex >= buffer.length) {
      int minSize;
      minSize = requiredIndex + 1;

      int newSize;
      newSize = powerOfTwo(minSize);

      if (newSize > maxBufferSize) {
        throw new UnsupportedOperationException("Implement me");
      }

      buffer = Arrays.copyOf(buffer, newSize);
    }

    System.arraycopy(bytes, 0, buffer, bufferIndex, length);

    bufferIndex += length;
  }

  // ##################################################################
  // # END: Http.Exchange API || response
  // ##################################################################

  public final boolean keepAlive() {
    return testBit(KEEP_ALIVE);
  }

  @Override
  public final boolean processed() {
    return testState(_PROCESSED);
  }

  // ##################################################################
  // # BEGIN: Http.Module support
  // ##################################################################

  final void matcherReset() {
    matcherIndex = 0;

    if (pathParams != null) {
      pathParams.clear();
    }
  }

  final boolean atEnd() {
    return matcherIndex == pathLimit;
  }

  final boolean exact(String other) {
    String value;
    value = path();

    boolean result;
    result = value.equals(other);

    matcherIndex += value.length();

    return result;
  }

  final boolean namedVariable(String name) {
    String value;
    value = path();

    int solidus;
    solidus = value.indexOf('/', matcherIndex);

    String varValue;

    if (solidus < 0) {
      varValue = value.substring(matcherIndex);
    } else {
      varValue = value.substring(matcherIndex, solidus);
    }

    matcherIndex += varValue.length();

    variable(name, varValue);

    return true;
  }

  final boolean region(String region) {
    String value;
    value = path();

    boolean result;
    result = value.regionMatches(matcherIndex, region, 0, region.length());

    matcherIndex += region.length();

    return result;
  }

  final boolean startsWithMatcher(String prefix) {
    String value;
    value = path();

    boolean result;
    result = value.startsWith(prefix);

    matcherIndex += prefix.length();

    return result;
  }

  private void variable(String name, String value) {
    if (pathParams == null) {
      pathParams = Util.createMap();
    }

    pathParams.put(name, value);
  }

  // ##################################################################
  // # END: Http.Module support
  // ##################################################################

  private void clearBit(int mask) {
    bitset &= ~mask;
  }

  private void setBit(int mask) {
    bitset |= mask;
  }

  private void setState(int value) {
    int bits;
    bits = bitset & BITS_MASK;

    bitset = bits | value;
  }

  private boolean testBit(int mask) {
    return (bitset & mask) != 0;
  }

  private boolean testState(int value) {
    int result;
    result = bitset & STATE_MASK;

    return result == value;
  }

  private String decode(String raw) {
    return URLDecoder.decode(raw, StandardCharsets.UTF_8);
  }

  private String encode(String value) {
    return URLEncoder.encode(value, StandardCharsets.UTF_8);
  }

  final void parseLine() throws IOException {
    int startIndex;
    startIndex = bufferIndex;

    byte needle;
    needle = Bytes.LF;

    while (true) {
      for (int i = startIndex; i < bufferLimit; i++) {
        byte maybe;
        maybe = buffer[i];

        if (maybe == needle) {
          lineLimit = i;

          return;
        }
      }

      // not inside buffer
      // let's try to read more data
      startIndex = bufferLimit;

      int writableLength;
      writableLength = buffer.length - bufferLimit;

      if (writableLength == 0) {
        // buffer is full, try to increase

        if (buffer.length == maxBufferSize) {
          // cannot increase...
          parseStatus = ParseStatus.OVERFLOW;

          return;
        }

        int newLength;
        newLength = buffer.length << 1;

        buffer = Arrays.copyOf(buffer, newLength);

        writableLength = buffer.length - bufferLimit;
      }

      int bytesRead;
      bytesRead = inputStream.read(buffer, bufferLimit, writableLength);

      if (bytesRead < 0) {
        // EOF
        parseStatus = ParseStatus.UNEXPECTED_EOF;

        return;
      }

      bufferLimit += bytesRead;
    }
  }

  final boolean matches(byte[] bytes) {
    int length;
    length = bytes.length;

    int toIndex;
    toIndex = bufferIndex + length;

    if (toIndex >= lineLimit) {
      // outside of line...
      return false;
    }

    boolean matches;
    matches = Arrays.equals(
        buffer, bufferIndex, toIndex,
        bytes, 0, length
    );

    if (matches) {
      bufferIndex += length;

      return true;
    } else {
      return false;
    }
  }

  final int indexOf(byte needle) {
    for (int i = bufferIndex; i < bufferLimit; i++) {
      byte maybe;
      maybe = buffer[i];

      if (maybe == needle) {
        return i;
      }
    }

    return -1;
  }

  final int indexOf(byte needleA, byte needleB) {
    for (int i = bufferIndex; i < bufferLimit; i++) {
      byte maybe;
      maybe = buffer[i];

      if (maybe == needleA) {
        return i;
      }

      if (maybe == needleB) {
        return i;
      }
    }

    return -1;
  }

  final boolean consumeIfEndOfLine() {
    if (bufferIndex < lineLimit) {
      byte next;
      next = buffer[bufferIndex++];

      if (next != Bytes.CR) {
        return false;
      }
    }

    if (bufferIndex != lineLimit) {
      return false;
    }

    // index immediately after LF
    bufferIndex++;

    return true;
  }

  final String bufferToString(int start, int end) {
    int length;
    length = end - start;

    return new String(buffer, start, length, StandardCharsets.UTF_8);
  }

  final boolean consumeIfEmptyLine() {
    int length;
    length = lineLimit - bufferIndex;

    if (length == 0) {
      bufferIndex++;

      return true;
    }

    if (length == 1) {
      byte cr;
      cr = buffer[bufferIndex];

      if (cr == Bytes.CR) {
        bufferIndex += 2;

        return true;
      }
    }

    return false;
  }

  final byte get(int index) {
    return buffer[index];
  }

  final boolean canBuffer(long contentLength) {
    int maxAvailable;
    maxAvailable = maxBufferSize - bufferIndex;

    return maxAvailable >= contentLength;
  }

  final int read(long contentLength) throws IOException {
    // unread bytes in buffer
    int unread;
    unread = bufferLimit - bufferIndex;

    if (unread >= contentLength) {
      // everything is in the buffer already -> do not read

      return 0;
    }

    // we assume canBuffer was invoked before this method...
    // i.e. max buffer size can hold everything
    int length;
    length = (int) contentLength;

    int requiredBufferLength;
    requiredBufferLength = bufferIndex + length;

    // must we increase our buffer?

    if (requiredBufferLength > buffer.length) {
      int newLength;
      newLength = powerOfTwo(requiredBufferLength);

      buffer = Arrays.copyOf(buffer, newLength);
    }

    // how many bytes must we read
    int mustReadCount;
    mustReadCount = length - unread;

    while (mustReadCount > 0) {
      int read;
      read = inputStream.read(buffer, bufferLimit, mustReadCount);

      if (read < 0) {
        return -1;
      }

      bufferLimit += read;

      mustReadCount -= read;
    }

    return length;
  }

  final long read(Path file, long contentLength) throws IOException {
    // max out buffer if necessary
    if (buffer.length < maxBufferSize) {
      buffer = Arrays.copyOf(buffer, maxBufferSize);
    }

    // unread bytes in buffer
    int unread;
    unread = bufferLimit - bufferIndex;

    // how many bytes must we read
    long mustReadCount;
    mustReadCount = contentLength - unread;

    try (OutputStream out = Files.newOutputStream(file)) {
      while (mustReadCount > 0) {
        // this is guaranteed to be an int value
        long available;
        available = buffer.length - bufferLimit;

        // this is guaranteed to be an int value
        long iteration;
        iteration = Math.min(available, mustReadCount);

        int read;
        read = inputStream.read(buffer, bufferLimit, (int) iteration);

        if (read < 0) {
          return -1;
        }

        bufferLimit += read;

        out.write(buffer, bufferIndex, bufferLimit - bufferIndex);

        bufferLimit = bufferIndex;

        mustReadCount -= read;
      }
    }

    return contentLength;
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy