1 | package com.renomad.minum.web; | |
2 | ||
3 | import java.io.ByteArrayOutputStream; | |
4 | import java.io.IOException; | |
5 | import java.io.RandomAccessFile; | |
6 | import java.nio.ByteBuffer; | |
7 | import java.nio.channels.FileChannel; | |
8 | import java.nio.charset.StandardCharsets; | |
9 | import java.nio.file.Files; | |
10 | import java.nio.file.Path; | |
11 | import java.util.Arrays; | |
12 | import java.util.HashMap; | |
13 | import java.util.Map; | |
14 | import java.util.Objects; | |
15 | import java.util.zip.GZIPOutputStream; | |
16 | ||
17 | import static com.renomad.minum.utils.FileUtils.badFilePathPatterns; | |
18 | import static com.renomad.minum.web.StatusLine.StatusCode.CODE_200_OK; | |
19 | import static com.renomad.minum.web.StatusLine.StatusCode.CODE_206_PARTIAL_CONTENT; | |
20 | ||
21 | /** | |
22 | * Represents an HTTP response. This is what will get sent back to the | |
23 | * client (that is, to the browser). There are a variety of overloads | |
24 | * of this record for different situations. The overarching paradigm is | |
25 | * to provide you high flexibility. | |
26 | * <p> | |
27 | * A response message is sent by a server to a client as a reply to its former request message. | |
28 | * </p> | |
29 | * <h3> | |
30 | * Response syntax | |
31 | * </h3> | |
32 | * <p> | |
33 | * A server sends response messages to the client, which consist of: | |
34 | * </p> | |
35 | * <ul> | |
36 | * <li> | |
37 | * a status line, consisting of the protocol version, a space, the | |
38 | * response status code, another space, a possibly empty reason | |
39 | * phrase, a carriage return and a line feed, e.g.: | |
40 | * <pre> | |
41 | * HTTP/1.1 200 OK | |
42 | * </pre> | |
43 | * </li> | |
44 | * | |
45 | * <li> | |
46 | * zero or more response header fields, each consisting of the case-insensitive | |
47 | * field name, a colon, optional leading whitespace, the field value, an | |
48 | * optional trailing whitespace and ending with a carriage return and a line feed, e.g.: | |
49 | * <pre> | |
50 | * Content-Type: text/html | |
51 | * </pre> | |
52 | * </li> | |
53 | * | |
54 | * <li> | |
55 | * an empty line, consisting of a carriage return and a line feed; | |
56 | * </li> | |
57 | * | |
58 | * <li> | |
59 | * an optional message body. | |
60 | * </li> | |
61 | *</ul> | |
62 | ||
63 | */ | |
64 | public final class Response implements IResponse { | |
65 | ||
66 | private final StatusLine.StatusCode statusCode; | |
67 | private final Map<String, String> extraHeaders; | |
68 | private final byte[] body; | |
69 | private final ThrowingConsumer<ISocketWrapper> outputGenerator; | |
70 | private final long bodyLength; | |
71 | ||
72 | ||
73 | /** | |
74 | * This is the constructor that provides access to all fields. It is not intended | |
75 | * to be used from outside this class. | |
76 | * @param extraHeaders extra headers we want to return with the response. | |
77 | * @param outputGenerator a {@link ThrowingConsumer} that will use a {@link ISocketWrapper} parameter | |
78 | * to send bytes on the wire back to the client. See the static factory methods | |
79 | * such as {@link #buildResponse(StatusLine.StatusCode, Map, byte[])} for more details on this. | |
80 | * @param bodyLength this is used to set the content-length header for the response. If this is | |
81 | * not provided, we set the header to "transfer-encoding: chunked", or in other words, streaming. | |
82 | */ | |
83 | Response(StatusLine.StatusCode statusCode, Map<String, String> extraHeaders, byte[] body, | |
84 | ThrowingConsumer<ISocketWrapper> outputGenerator, long bodyLength) { | |
85 | this.statusCode = statusCode; | |
86 | this.extraHeaders = new HashMap<>(extraHeaders); | |
87 | this.body = body; | |
88 | this.outputGenerator = outputGenerator; | |
89 | this.bodyLength = bodyLength; | |
90 | } | |
91 | ||
92 | /** | |
93 | * This factory method is intended for situations where the user wishes to stream data | |
94 | * but lacks the content length. This is only for unusual situations where the developer | |
95 | * needs the extra control. In most cases, other methods are more suitable. | |
96 | * @param extraHeaders any extra headers for the response, such as the content-type | |
97 | * @param outputGenerator a function that will be given a {@link ISocketWrapper}, providing the | |
98 | * ability to send bytes on the socket. | |
99 | */ | |
100 | public static IResponse buildStreamingResponse(StatusLine.StatusCode statusCode, Map<String, String> extraHeaders, ThrowingConsumer<ISocketWrapper> outputGenerator) { | |
101 |
1
1. buildStreamingResponse : replaced return value with null for com/renomad/minum/web/Response::buildStreamingResponse → KILLED |
return new Response(statusCode, extraHeaders, null, outputGenerator, 0); |
102 | } | |
103 | ||
104 | /** | |
105 | * Similar to {@link Response#buildStreamingResponse(StatusLine.StatusCode, Map, ThrowingConsumer)} but here we know | |
106 | * the body length, so that will be sent to the client as content-length. | |
107 | * @param extraHeaders any extra headers for the response, such as the content-type | |
108 | * @param outputGenerator a function that will be given a {@link ISocketWrapper}, providing the | |
109 | * ability to send bytes on the socket. | |
110 | * @param bodyLength the length, in bytes, of the data to be sent to the client | |
111 | */ | |
112 | public static IResponse buildStreamingResponse(StatusLine.StatusCode statusCode, Map<String, String> extraHeaders, ThrowingConsumer<ISocketWrapper> outputGenerator, long bodyLength) { | |
113 |
1
1. buildStreamingResponse : replaced return value with null for com/renomad/minum/web/Response::buildStreamingResponse → KILLED |
return new Response(statusCode, extraHeaders, null, outputGenerator, bodyLength); |
114 | } | |
115 | ||
116 | /** | |
117 | * A constructor for situations where the developer wishes to send a small (less than a megabyte) byte array | |
118 | * to the client. If there is need to send something of larger size, choose one these | |
119 | * alternate constructors: | |
120 | * FileChannel - for sending a large file: {@link Response#buildLargeFileResponse(Map, String, Headers)} | |
121 | * Streaming - for more custom streaming control with a known body size: {@link Response#buildStreamingResponse(StatusLine.StatusCode, Map, ThrowingConsumer, long)} | |
122 | * Streaming - for more custom streaming control with body size unknown: {@link Response#buildStreamingResponse(StatusLine.StatusCode, Map, ThrowingConsumer)} | |
123 | * | |
124 | * @param extraHeaders any extra headers for the response, such as the content-type. | |
125 | */ | |
126 | public static IResponse buildResponse(StatusLine.StatusCode statusCode, Map<String, String> extraHeaders, byte[] body) { | |
127 |
2
1. lambda$buildResponse$0 : removed call to com/renomad/minum/web/Response::sendByteArrayResponse → KILLED 2. buildResponse : replaced return value with null for com/renomad/minum/web/Response::buildResponse → KILLED |
return new Response(statusCode, extraHeaders, body, socketWrapper -> sendByteArrayResponse(socketWrapper, body), body.length); |
128 | } | |
129 | ||
130 | /** | |
131 | * Build an ordinary response, with a known body | |
132 | * @param extraHeaders extra HTTP headers, like <pre>content-type: text/html</pre> | |
133 | */ | |
134 | public static IResponse buildResponse(StatusLine.StatusCode statusCode, Map<String, String> extraHeaders, String body) { | |
135 | byte[] bytes = body.getBytes(StandardCharsets.UTF_8); | |
136 |
2
1. lambda$buildResponse$1 : removed call to com/renomad/minum/web/Response::sendByteArrayResponse → KILLED 2. buildResponse : replaced return value with null for com/renomad/minum/web/Response::buildResponse → KILLED |
return new Response(statusCode, extraHeaders, bytes, socketWrapper -> sendByteArrayResponse(socketWrapper, bytes), bytes.length); |
137 | } | |
138 | ||
139 | public static IResponse buildLargeFileResponse(Map<String, String> extraHeaders, String filePath, Headers requestHeaders) throws IOException { | |
140 |
1
1. buildLargeFileResponse : negated conditional → KILLED |
if (badFilePathPatterns.matcher(filePath).find()) { |
141 | throw new WebServerException(String.format("Bad path requested at readFile: %s", filePath)); | |
142 | } | |
143 | ||
144 | Map<String, String> adjustedHeaders = new HashMap<>(extraHeaders); | |
145 | long fileSize = Files.size(Path.of(filePath)); | |
146 | var range = new Range(requestHeaders, fileSize); | |
147 | StatusLine.StatusCode responseCode = CODE_200_OK; | |
148 | long length = fileSize; | |
149 |
1
1. buildLargeFileResponse : negated conditional → TIMED_OUT |
if (range.hasRangeHeader()) { |
150 | long offset = range.getOffset(); | |
151 | length = range.getLength(); | |
152 |
2
1. buildLargeFileResponse : Replaced long addition with subtraction → KILLED 2. buildLargeFileResponse : Replaced long subtraction with addition → KILLED |
var lastIndex = (offset + length) - 1; |
153 | adjustedHeaders.put("Content-Range", String.format("bytes %d-%d/%d", offset, lastIndex, fileSize)); | |
154 | responseCode = CODE_206_PARTIAL_CONTENT; | |
155 | } | |
156 | ||
157 | ThrowingConsumer<ISocketWrapper> outputGenerator = socketWrapper -> { | |
158 | try (RandomAccessFile reader = new RandomAccessFile(filePath, "r")) { | |
159 |
1
1. lambda$buildLargeFileResponse$2 : removed call to java/io/RandomAccessFile::seek → SURVIVED |
reader.seek(range.getOffset()); |
160 | var fileChannel = reader.getChannel(); | |
161 |
1
1. lambda$buildLargeFileResponse$2 : removed call to com/renomad/minum/web/Response::sendFileChannelResponse → TIMED_OUT |
sendFileChannelResponse(socketWrapper, fileChannel, range.getLength()); |
162 | } | |
163 | }; | |
164 | ||
165 |
1
1. buildLargeFileResponse : replaced return value with null for com/renomad/minum/web/Response::buildLargeFileResponse → KILLED |
return new Response(responseCode, adjustedHeaders, null, outputGenerator, length); |
166 | } | |
167 | ||
168 | /** | |
169 | * Build a {@link Response} with just a status code and headers, without a body | |
170 | * @param extraHeaders extra HTTP headers | |
171 | */ | |
172 | public static IResponse buildLeanResponse(StatusLine.StatusCode statusCode, Map<String, String> extraHeaders) { | |
173 |
1
1. buildLeanResponse : replaced return value with null for com/renomad/minum/web/Response::buildLeanResponse → KILLED |
return new Response(statusCode, extraHeaders, null, socketWrapper -> {}, 0); |
174 | } | |
175 | ||
176 | /** | |
177 | * Build a {@link Response} with only a status code, with no body and no extra headers. | |
178 | */ | |
179 | public static IResponse buildLeanResponse(StatusLine.StatusCode statusCode) { | |
180 |
1
1. buildLeanResponse : replaced return value with null for com/renomad/minum/web/Response::buildLeanResponse → KILLED |
return new Response(statusCode, Map.of(), null, socketWrapper -> {}, 0); |
181 | } | |
182 | ||
183 | ||
184 | /** | |
185 | * A helper method to create a response that returns a | |
186 | * 303 status code ("see other"). Provide a url that will | |
187 | * be handed to the browser. This url may be relative or absolute. | |
188 | */ | |
189 | public static IResponse redirectTo(String locationUrl) { | |
190 |
1
1. redirectTo : replaced return value with null for com/renomad/minum/web/Response::redirectTo → KILLED |
return buildLeanResponse(StatusLine.StatusCode.CODE_303_SEE_OTHER, Map.of("location", locationUrl)); |
191 | } | |
192 | ||
193 | /** | |
194 | * If you are returning HTML text with a 200 ok, this is a helper that | |
195 | * lets you skip some of the boilerplate. This version of the helper | |
196 | * lets you add extra headers on top of the basic content-type headers | |
197 | * that are needed to specify this is HTML. | |
198 | */ | |
199 | public static IResponse htmlOk(String body, Map<String, String> extraHeaders) { | |
200 | var headers = new HashMap<String, String>(); | |
201 | headers.put("Content-Type", "text/html; charset=UTF-8"); | |
202 |
1
1. htmlOk : removed call to java/util/HashMap::putAll → KILLED |
headers.putAll(extraHeaders); |
203 |
1
1. htmlOk : replaced return value with null for com/renomad/minum/web/Response::htmlOk → KILLED |
return buildResponse(StatusLine.StatusCode.CODE_200_OK, headers, body); |
204 | } | |
205 | ||
206 | /** | |
207 | * If you are returning HTML text with a 200 ok, this is a helper that | |
208 | * lets you skip some of the boilerplate. | |
209 | */ | |
210 | public static IResponse htmlOk(String body) { | |
211 |
1
1. htmlOk : replaced return value with null for com/renomad/minum/web/Response::htmlOk → KILLED |
return htmlOk(body, Map.of()); |
212 | } | |
213 | ||
214 | @Override | |
215 | public Map<String, String> getExtraHeaders() { | |
216 |
1
1. getExtraHeaders : replaced return value with Collections.emptyMap for com/renomad/minum/web/Response::getExtraHeaders → KILLED |
return new HashMap<>(extraHeaders); |
217 | } | |
218 | ||
219 | @Override | |
220 | public StatusLine.StatusCode getStatusCode() { | |
221 |
1
1. getStatusCode : replaced return value with null for com/renomad/minum/web/Response::getStatusCode → KILLED |
return statusCode; |
222 | } | |
223 | ||
224 | /** | |
225 | * Gets the length of the body for this response. If the body | |
226 | * is an array of bytes set by the user, we grab this value by the | |
227 | * length() method. If the outgoing data is set by a lambda, the user | |
228 | * will set the bodyLength value. | |
229 | */ | |
230 | long getBodyLength() { | |
231 |
1
1. getBodyLength : negated conditional → KILLED |
if (body != null) { |
232 |
1
1. getBodyLength : replaced long return with 0 for com/renomad/minum/web/Response::getBodyLength → KILLED |
return body.length; |
233 | } else { | |
234 |
1
1. getBodyLength : replaced long return with 0 for com/renomad/minum/web/Response::getBodyLength → KILLED |
return bodyLength; |
235 | } | |
236 | } | |
237 | ||
238 | /** | |
239 | * By calling this method with a {@link ISocketWrapper} parameter, the method | |
240 | * will send bytes on the associated socket. | |
241 | */ | |
242 | void sendBody(ISocketWrapper sw) throws IOException { | |
243 | try { | |
244 |
1
1. sendBody : removed call to com/renomad/minum/web/ThrowingConsumer::accept → KILLED |
outputGenerator.accept(sw); |
245 | } catch (Exception ex) { | |
246 | throw new IOException(ex.getMessage(), ex); | |
247 | } | |
248 | } | |
249 | ||
250 | /** | |
251 | * put bytes from a file into the socket, sending to the client | |
252 | * @param fileChannel the file we are reading from, based on a {@link RandomAccessFile} | |
253 | * @param length the number of bytes to send. May be less than the full length of this {@link FileChannel} | |
254 | */ | |
255 | private static void sendFileChannelResponse(ISocketWrapper sw, FileChannel fileChannel, long length) throws IOException { | |
256 | try { | |
257 | int bufferSize = 8 * 1024; | |
258 | ByteBuffer buff = ByteBuffer.allocate(bufferSize); | |
259 | long countBytesLeftToSend = length; | |
260 | while (true) { | |
261 | int countBytesRead = fileChannel.read(buff); | |
262 |
2
1. sendFileChannelResponse : changed conditional boundary → TIMED_OUT 2. sendFileChannelResponse : negated conditional → KILLED |
if (countBytesRead <= 0) { |
263 | break; | |
264 | } else { | |
265 |
2
1. sendFileChannelResponse : changed conditional boundary → SURVIVED 2. sendFileChannelResponse : negated conditional → KILLED |
if (countBytesLeftToSend < countBytesRead) { |
266 |
1
1. sendFileChannelResponse : removed call to com/renomad/minum/web/ISocketWrapper::send → KILLED |
sw.send(buff.array(), 0, (int)countBytesLeftToSend); |
267 | break; | |
268 | } else { | |
269 |
1
1. sendFileChannelResponse : removed call to com/renomad/minum/web/ISocketWrapper::send → TIMED_OUT |
sw.send(buff.array(), 0, countBytesRead); |
270 | } | |
271 | buff.clear(); | |
272 | } | |
273 |
1
1. sendFileChannelResponse : Replaced long subtraction with addition → SURVIVED |
countBytesLeftToSend -= countBytesRead; |
274 | } | |
275 | } finally { | |
276 |
1
1. sendFileChannelResponse : removed call to java/nio/channels/FileChannel::close → KILLED |
fileChannel.close(); |
277 | } | |
278 | } | |
279 | ||
280 | private static void sendByteArrayResponse(ISocketWrapper sw, byte[] body) throws IOException { | |
281 |
1
1. sendByteArrayResponse : removed call to com/renomad/minum/web/ISocketWrapper::send → KILLED |
sw.send(body); |
282 | } | |
283 | ||
284 | @Override | |
285 | public byte[] getBody() { | |
286 |
1
1. getBody : replaced return value with null for com/renomad/minum/web/Response::getBody → KILLED |
return body; |
287 | } | |
288 | ||
289 | /** | |
290 | * Compress the data in this body using gzip. | |
291 | * <br> | |
292 | * This operates by getting the body field from this instance of {@link Response} and | |
293 | * creating a new Response with the compressed data. | |
294 | */ | |
295 | Response compressBody() throws IOException { | |
296 | ||
297 | ByteArrayOutputStream out = new ByteArrayOutputStream(); | |
298 | var gos = new GZIPOutputStream(out); | |
299 |
1
1. compressBody : removed call to java/util/zip/GZIPOutputStream::write → SURVIVED |
gos.write(body); |
300 |
1
1. compressBody : removed call to java/util/zip/GZIPOutputStream::finish → SURVIVED |
gos.finish(); |
301 |
1
1. compressBody : replaced return value with null for com/renomad/minum/web/Response::compressBody → KILLED |
return (Response)Response.buildResponse( |
302 | statusCode, | |
303 | extraHeaders, | |
304 | out.toByteArray() | |
305 | ); | |
306 | } | |
307 | ||
308 | @Override | |
309 | public boolean equals(Object o) { | |
310 |
2
1. equals : replaced boolean return with false for com/renomad/minum/web/Response::equals → KILLED 2. equals : negated conditional → KILLED |
if (this == o) return true; |
311 |
3
1. equals : negated conditional → KILLED 2. equals : replaced boolean return with true for com/renomad/minum/web/Response::equals → KILLED 3. equals : negated conditional → KILLED |
if (o == null || getClass() != o.getClass()) return false; |
312 | Response response = (Response) o; | |
313 |
5
1. equals : negated conditional → KILLED 2. equals : replaced boolean return with true for com/renomad/minum/web/Response::equals → KILLED 3. equals : negated conditional → KILLED 4. equals : negated conditional → KILLED 5. equals : negated conditional → KILLED |
return bodyLength == response.bodyLength && statusCode == response.statusCode && Objects.equals(extraHeaders, response.extraHeaders) && Arrays.equals(body, response.body); |
314 | } | |
315 | ||
316 | @Override | |
317 | public int hashCode() { | |
318 | int result = Objects.hash(statusCode, extraHeaders, bodyLength); | |
319 |
2
1. hashCode : Replaced integer addition with subtraction → SURVIVED 2. hashCode : Replaced integer multiplication with division → KILLED |
result = 31 * result + Arrays.hashCode(body); |
320 |
1
1. hashCode : replaced int return with 0 for com/renomad/minum/web/Response::hashCode → KILLED |
return result; |
321 | } | |
322 | ||
323 | @Override | |
324 | public String toString() { | |
325 |
1
1. toString : replaced return value with "" for com/renomad/minum/web/Response::toString → KILLED |
return "Response{" + |
326 | "statusCode=" + statusCode + | |
327 | ", extraHeaders=" + extraHeaders + | |
328 | ", bodyLength=" + bodyLength + | |
329 | '}'; | |
330 | } | |
331 | } | |
Mutations | ||
101 |
1.1 |
|
113 |
1.1 |
|
127 |
1.1 2.2 |
|
136 |
1.1 2.2 |
|
140 |
1.1 |
|
149 |
1.1 |
|
152 |
1.1 2.2 |
|
159 |
1.1 |
|
161 |
1.1 |
|
165 |
1.1 |
|
173 |
1.1 |
|
180 |
1.1 |
|
190 |
1.1 |
|
202 |
1.1 |
|
203 |
1.1 |
|
211 |
1.1 |
|
216 |
1.1 |
|
221 |
1.1 |
|
231 |
1.1 |
|
232 |
1.1 |
|
234 |
1.1 |
|
244 |
1.1 |
|
262 |
1.1 2.2 |
|
265 |
1.1 2.2 |
|
266 |
1.1 |
|
269 |
1.1 |
|
273 |
1.1 |
|
276 |
1.1 |
|
281 |
1.1 |
|
286 |
1.1 |
|
299 |
1.1 |
|
300 |
1.1 |
|
301 |
1.1 |
|
310 |
1.1 2.2 |
|
311 |
1.1 2.2 3.3 |
|
313 |
1.1 2.2 3.3 4.4 5.5 |
|
319 |
1.1 2.2 |
|
320 |
1.1 |
|
325 |
1.1 |