StatusLine.java
package com.renomad.minum.web;
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.renomad.minum.utils.Invariants.mustBeTrue;
/**
* This class represents the text that is sent back in a {@link Response}
*/
public record StatusLine(StatusCode status, HttpVersion version, String rawValue) {
static final StatusLine EMPTY = new StatusLine(StatusCode.NULL, HttpVersion.NONE, "");
/**
* This is the regex used to analyze a status line sent by the server and
* read by the client. Servers will send messages like: "HTTP/1.1 200 OK" or "HTTP/1.1 500 Internal Server Error"
*/
static final String statusLinePattern = "^HTTP/(...) (\\d{3}) (.*)$";
static final Pattern statusLineRegex = Pattern.compile(statusLinePattern);
/**
* See <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status">Status Codes</a>
*/
public enum StatusCode{
/* Information responses */
CODE_100_CONTINUE(100, "CONTINUE"),
CODE_101_SWITCHING_PROTOCOLS(101, "SWITCHING PROTOCOLS"),
CODE_102_PROCESSING(102, "PROCESSING"),
CODE_103_EARLY_HINTS(103, "EARLY HINTS"),
/* Successful responses (200 – 299) */
CODE_200_OK(200, "OK"),
CODE_201_CREATED(201, "CREATED"),
CODE_202_ACCEPTED(202, "ACCEPTED"),
CODE_203_NON_AUTHORITATIVE_INFORMATION(203, "NON-AUTHORITATIVE INFORMATION"),
CODE_204_NO_CONTENT(204, "NO CONTENT"),
CODE_205_RESET_CONTENT(205, "RESET CONTENT"),
CODE_206_PARTIAL_CONTENT(206, "PARTIAL CONTENT"),
CODE_207_MULTI_STATUS(207, "MULTI-STATUS"),
CODE_208_ALREADY_REPORTED(208, "ALREADY REPORTED"),
CODE_226_IM_USED(226, "IM USED"),
/* Redirection messages (300 – 399) */
CODE_300_MULTIPLE_CHOICES(300, "MULTIPLE CHOICES"),
CODE_301_MOVED_PERMANENTLY(301, "MOVED PERMANENTLY"),
CODE_302_FOUND(302, "FOUND"),
/**
* Used a lot after receiving a post response. The pattern is to
* receive the post, then redirect to a new page. See <a href="https://en.wikipedia.org/wiki/Post/Redirect/Get">...</a>
*/
CODE_303_SEE_OTHER(303, "SEE OTHER"),
CODE_304_NOT_MODIFIED(304, "NOT MODIFIED"),
CODE_305_USE_PROXY(305, "USE PROXY"),
CODE_306_UNUSED(306, "UNUSED"),
CODE_307_TEMPORARY_REDIRECT(307, "TEMPORARY REDIRECT"),
CODE_308_PERMANENT_REDIRECT(308, "PERMANENT REDIRECT"),
/* Client error responses (400 – 499) */
CODE_400_BAD_REQUEST(400, "BAD REQUEST"),
CODE_401_UNAUTHORIZED(401, "UNAUTHORIZED"),
CODE_402_PAYMENT_REQUIRED(402, "PAYMENT REQUIRED"),
CODE_403_FORBIDDEN(403, "FORBIDDEN"),
CODE_404_NOT_FOUND(404, "NOT FOUND"),
CODE_405_METHOD_NOT_ALLOWED(405, "METHOD NOT ALLOWED"),
CODE_406_NOT_ACCEPTABLE(406, "NOT ACCEPTABLE"),
CODE_407_PROXY_AUTHENTICATION_REQUIRED(407, "PROXY AUTHENTICATION REQUIRED"),
CODE_408_REQUEST_TIMEOUT(408, "REQUEST TIMEOUT"),
CODE_409_CONFLICT(409, "CONFLICT"),
CODE_410_GONE(410, "GONE"),
CODE_411_LENGTH_REQUIRED(411, "LENGTH REQUIRED"),
CODE_412_PRECONDITION_FAILED(412, "PRECONDITION FAILED"),
CODE_413_PAYLOAD_TOO_LARGE(413, "PAYLOAD TOO LARGE"),
CODE_414_URI_TOO_LONG(414, "URI TOO LONG"),
CODE_415_UNSUPPORTED_MEDIA_TYPE(415, "UNSUPPORTED MEDIA TYPE"),
CODE_416_RANGE_NOT_SATISFIABLE(416, "RANGE NOT SATISFIABLE"),
CODE_417_EXPECTATION_FAILED(417, "EXPECTATION FAILED"),
CODE_418_IM_A_TEAPOT(418, "IM A TEAPOT"),
CODE_421_MISDIRECTED_REQUEST(421, "MISDIRECTED REQUEST"),
CODE_422_UNPROCESSABLE_CONTENT(422, "UNPROCESSABLE CONTENT"),
CODE_423_LOCKED(423, "LOCKED"),
CODE_424_FAILED_DEPENDENCY(424, "FAILED DEPENDENCY"),
CODE_425_TOO_EARLY(425, "TOO EARLY"),
CODE_426_UPGRADE_REQUIRED(426, "UPGRADE REQUIRED"),
CODE_428_PRECONDITION_REQUIRED(428, "PRECONDITION REQUIRED"),
CODE_429_TOO_MANY_REQUESTS(429, "TOO MANY REQUESTS"),
CODE_431_REQUEST_HEADER_FIELDS_TOO_LARGE(431, "REQUEST HEADER FIELDS TOO LARGE"),
CODE_451_UNAVAILABLE_FOR_LEGAL_REASONS(451, "UNAVAILABLE FOR LEGAL REASONS"),
/* Server error responses (500 – 599) */
CODE_500_INTERNAL_SERVER_ERROR(500, "INTERNAL SERVER ERROR"),
CODE_501_NOT_IMPLEMENTED(501, "NOT IMPLEMENTED"),
CODE_502_BAD_GATEWAY(502, "BAD GATEWAY"),
CODE_503_SERVICE_UNAVAILABLE(503, "SERVICE UNAVAILABLE"),
CODE_504_GATEWAY_TIMEOUT(504, "GATEWAY TIMEOUT"),
CODE_505_HTTP_VERSION_NOT_SUPPORTED(505, "HTTP VERSION NOT SUPPORTED"),
CODE_506_VARIANT_ALSO_NEGOTIATES(506, "VARIANT ALSO NEGOTIATES"),
CODE_507_INSUFFICIENT_STORAGE(507, "INSUFFICIENT STORAGE"),
CODE_508_LOOP_DETECTED(508, "LOOP DETECTED"),
CODE_510_NOT_EXTENDED(510, "NOT EXTENDED"),
CODE_511_NETWORK_AUTHENTICATION_REQUIRED(511, "NETWORK AUTHENTICATION REQUIRED"),
/**
* The null object, meant to represent "no status code"
*/
NULL(0, "NULL OBJECT")
;
public final int code;
public final String shortDescription;
StatusCode(int code, String shortDescription) {
this.code = code;
this.shortDescription = shortDescription;
}
static StatusCode findByCode(int code) {
return Arrays.stream(StatusCode.values())
.filter(x -> x.code == code)
.findFirst()
.orElseThrow();
}
}
/**
* Parses a string value of a status line from an HTTP
* server. If the input value is null or empty, we'll
* return a {@link StatusLine} with null-object values
*/
public static StatusLine extractStatusLine(String value) {
if (value == null || value.isBlank()) {
return StatusLine.EMPTY;
}
Matcher mr = StatusLine.statusLineRegex.matcher(value);
mustBeTrue(mr.matches(), String.format("%s must match the statusLinePattern: %s", value, statusLinePattern));
String version = mr.group(1);
HttpVersion httpVersion = switch (version) {
case "1.1" -> HttpVersion.ONE_DOT_ONE;
case "1.0" -> HttpVersion.ONE_DOT_ZERO;
default -> throw new WebServerException(String.format("HTTP version was not an acceptable value. Given: %s", version));
};
StatusCode status = StatusCode.findByCode(Integer.parseInt(mr.group(2)));
return new StatusLine(status, httpVersion, value);
}
}