1 | package com.renomad.minum.web; | |
2 | ||
3 | import java.util.Arrays; | |
4 | import java.util.regex.Matcher; | |
5 | import java.util.regex.Pattern; | |
6 | ||
7 | import static com.renomad.minum.utils.Invariants.mustBeTrue; | |
8 | ||
9 | /** | |
10 | * This class represents the text that is sent back in a {@link Response} | |
11 | */ | |
12 | public record StatusLine(StatusCode status, HttpVersion version, String rawValue) { | |
13 | ||
14 | static final StatusLine EMPTY = new StatusLine(StatusCode.NULL, HttpVersion.NONE, ""); | |
15 | ||
16 | /** | |
17 | * This is the regex used to analyze a status line sent by the server and | |
18 | * read by the client. Servers will send messages like: "HTTP/1.1 200 OK" or "HTTP/1.1 500 Internal Server Error" | |
19 | */ | |
20 | static final String statusLinePattern = "^HTTP/(...) (\\d{3}) (.*)$"; | |
21 | static final Pattern statusLineRegex = Pattern.compile(statusLinePattern); | |
22 | ||
23 | /** | |
24 | * See <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status">Status Codes</a> | |
25 | */ | |
26 | public enum StatusCode{ | |
27 | ||
28 | /* Information responses */ | |
29 | CODE_100_CONTINUE(100, "CONTINUE"), | |
30 | CODE_101_SWITCHING_PROTOCOLS(101, "SWITCHING PROTOCOLS"), | |
31 | CODE_102_PROCESSING(102, "PROCESSING"), | |
32 | CODE_103_EARLY_HINTS(103, "EARLY HINTS"), | |
33 | ||
34 | ||
35 | /* Successful responses (200 – 299) */ | |
36 | ||
37 | CODE_200_OK(200, "OK"), | |
38 | CODE_201_CREATED(201, "CREATED"), | |
39 | CODE_202_ACCEPTED(202, "ACCEPTED"), | |
40 | CODE_203_NON_AUTHORITATIVE_INFORMATION(203, "NON-AUTHORITATIVE INFORMATION"), | |
41 | CODE_204_NO_CONTENT(204, "NO CONTENT"), | |
42 | CODE_205_RESET_CONTENT(205, "RESET CONTENT"), | |
43 | CODE_206_PARTIAL_CONTENT(206, "PARTIAL CONTENT"), | |
44 | CODE_207_MULTI_STATUS(207, "MULTI-STATUS"), | |
45 | CODE_208_ALREADY_REPORTED(208, "ALREADY REPORTED"), | |
46 | CODE_226_IM_USED(226, "IM USED"), | |
47 | ||
48 | /* Redirection messages (300 – 399) */ | |
49 | ||
50 | CODE_300_MULTIPLE_CHOICES(300, "MULTIPLE CHOICES"), | |
51 | CODE_301_MOVED_PERMANENTLY(301, "MOVED PERMANENTLY"), | |
52 | CODE_302_FOUND(302, "FOUND"), | |
53 | ||
54 | /** | |
55 | * Used a lot after receiving a post response. The pattern is to | |
56 | * receive the post, then redirect to a new page. See <a href="https://en.wikipedia.org/wiki/Post/Redirect/Get">...</a> | |
57 | */ | |
58 | CODE_303_SEE_OTHER(303, "SEE OTHER"), | |
59 | CODE_304_NOT_MODIFIED(304, "NOT MODIFIED"), | |
60 | CODE_305_USE_PROXY(305, "USE PROXY"), | |
61 | CODE_306_UNUSED(306, "UNUSED"), | |
62 | CODE_307_TEMPORARY_REDIRECT(307, "TEMPORARY REDIRECT"), | |
63 | CODE_308_PERMANENT_REDIRECT(308, "PERMANENT REDIRECT"), | |
64 | ||
65 | /* Client error responses (400 – 499) */ | |
66 | ||
67 | CODE_400_BAD_REQUEST(400, "BAD REQUEST"), | |
68 | CODE_401_UNAUTHORIZED(401, "UNAUTHORIZED"), | |
69 | CODE_402_PAYMENT_REQUIRED(402, "PAYMENT REQUIRED"), | |
70 | CODE_403_FORBIDDEN(403, "FORBIDDEN"), | |
71 | CODE_404_NOT_FOUND(404, "NOT FOUND"), | |
72 | CODE_405_METHOD_NOT_ALLOWED(405, "METHOD NOT ALLOWED"), | |
73 | CODE_406_NOT_ACCEPTABLE(406, "NOT ACCEPTABLE"), | |
74 | CODE_407_PROXY_AUTHENTICATION_REQUIRED(407, "PROXY AUTHENTICATION REQUIRED"), | |
75 | CODE_408_REQUEST_TIMEOUT(408, "REQUEST TIMEOUT"), | |
76 | CODE_409_CONFLICT(409, "CONFLICT"), | |
77 | CODE_410_GONE(410, "GONE"), | |
78 | CODE_411_LENGTH_REQUIRED(411, "LENGTH REQUIRED"), | |
79 | CODE_412_PRECONDITION_FAILED(412, "PRECONDITION FAILED"), | |
80 | CODE_413_PAYLOAD_TOO_LARGE(413, "PAYLOAD TOO LARGE"), | |
81 | CODE_414_URI_TOO_LONG(414, "URI TOO LONG"), | |
82 | CODE_415_UNSUPPORTED_MEDIA_TYPE(415, "UNSUPPORTED MEDIA TYPE"), | |
83 | CODE_416_RANGE_NOT_SATISFIABLE(416, "RANGE NOT SATISFIABLE"), | |
84 | CODE_417_EXPECTATION_FAILED(417, "EXPECTATION FAILED"), | |
85 | CODE_418_IM_A_TEAPOT(418, "IM A TEAPOT"), | |
86 | CODE_421_MISDIRECTED_REQUEST(421, "MISDIRECTED REQUEST"), | |
87 | CODE_422_UNPROCESSABLE_CONTENT(422, "UNPROCESSABLE CONTENT"), | |
88 | CODE_423_LOCKED(423, "LOCKED"), | |
89 | CODE_424_FAILED_DEPENDENCY(424, "FAILED DEPENDENCY"), | |
90 | CODE_425_TOO_EARLY(425, "TOO EARLY"), | |
91 | CODE_426_UPGRADE_REQUIRED(426, "UPGRADE REQUIRED"), | |
92 | CODE_428_PRECONDITION_REQUIRED(428, "PRECONDITION REQUIRED"), | |
93 | CODE_429_TOO_MANY_REQUESTS(429, "TOO MANY REQUESTS"), | |
94 | CODE_431_REQUEST_HEADER_FIELDS_TOO_LARGE(431, "REQUEST HEADER FIELDS TOO LARGE"), | |
95 | CODE_451_UNAVAILABLE_FOR_LEGAL_REASONS(451, "UNAVAILABLE FOR LEGAL REASONS"), | |
96 | ||
97 | /* Server error responses (500 – 599) */ | |
98 | ||
99 | CODE_500_INTERNAL_SERVER_ERROR(500, "INTERNAL SERVER ERROR"), | |
100 | CODE_501_NOT_IMPLEMENTED(501, "NOT IMPLEMENTED"), | |
101 | CODE_502_BAD_GATEWAY(502, "BAD GATEWAY"), | |
102 | CODE_503_SERVICE_UNAVAILABLE(503, "SERVICE UNAVAILABLE"), | |
103 | CODE_504_GATEWAY_TIMEOUT(504, "GATEWAY TIMEOUT"), | |
104 | CODE_505_HTTP_VERSION_NOT_SUPPORTED(505, "HTTP VERSION NOT SUPPORTED"), | |
105 | CODE_506_VARIANT_ALSO_NEGOTIATES(506, "VARIANT ALSO NEGOTIATES"), | |
106 | CODE_507_INSUFFICIENT_STORAGE(507, "INSUFFICIENT STORAGE"), | |
107 | CODE_508_LOOP_DETECTED(508, "LOOP DETECTED"), | |
108 | CODE_510_NOT_EXTENDED(510, "NOT EXTENDED"), | |
109 | CODE_511_NETWORK_AUTHENTICATION_REQUIRED(511, "NETWORK AUTHENTICATION REQUIRED"), | |
110 | ||
111 | /** | |
112 | * The null object, meant to represent "no status code" | |
113 | */ | |
114 | NULL(0, "NULL OBJECT") | |
115 | ; | |
116 | ||
117 | public final int code; | |
118 | public final String shortDescription; | |
119 | ||
120 | StatusCode(int code, String shortDescription) { | |
121 | this.code = code; | |
122 | this.shortDescription = shortDescription; | |
123 | } | |
124 | ||
125 | static StatusCode findByCode(int code) { | |
126 |
1
1. findByCode : replaced return value with null for com/renomad/minum/web/StatusLine$StatusCode::findByCode → KILLED |
return Arrays.stream(StatusCode.values()) |
127 |
2
1. lambda$findByCode$0 : replaced boolean return with true for com/renomad/minum/web/StatusLine$StatusCode::lambda$findByCode$0 → KILLED 2. lambda$findByCode$0 : negated conditional → KILLED |
.filter(x -> x.code == code) |
128 | .findFirst() | |
129 | .orElseThrow(); | |
130 | } | |
131 | } | |
132 | ||
133 | /** | |
134 | * Parses a string value of a status line from an HTTP | |
135 | * server. If the input value is null or empty, we'll | |
136 | * return a {@link StatusLine} with null-object values | |
137 | */ | |
138 | public static StatusLine extractStatusLine(String value) { | |
139 |
2
1. extractStatusLine : negated conditional → KILLED 2. extractStatusLine : negated conditional → KILLED |
if (value == null || value.isBlank()) { |
140 |
1
1. extractStatusLine : replaced return value with null for com/renomad/minum/web/StatusLine::extractStatusLine → KILLED |
return StatusLine.EMPTY; |
141 | } | |
142 | Matcher mr = StatusLine.statusLineRegex.matcher(value); | |
143 | mustBeTrue(mr.matches(), String.format("%s must match the statusLinePattern: %s", value, statusLinePattern)); | |
144 | String version = mr.group(1); | |
145 | HttpVersion httpVersion = switch (version) { | |
146 | case "1.1" -> HttpVersion.ONE_DOT_ONE; | |
147 | case "1.0" -> HttpVersion.ONE_DOT_ZERO; | |
148 | default -> throw new WebServerException(String.format("HTTP version was not an acceptable value. Given: %s", version)); | |
149 | }; | |
150 | StatusCode status = StatusCode.findByCode(Integer.parseInt(mr.group(2))); | |
151 | ||
152 |
1
1. extractStatusLine : replaced return value with null for com/renomad/minum/web/StatusLine::extractStatusLine → KILLED |
return new StatusLine(status, httpVersion, value); |
153 | } | |
154 | } | |
Mutations | ||
126 |
1.1 |
|
127 |
1.1 2.2 |
|
139 |
1.1 2.2 |
|
140 |
1.1 |
|
152 |
1.1 |