Response.java

1
package com.renomad.minum.web;
2
3
import com.renomad.minum.utils.FileUtils;
4
5
import java.io.ByteArrayOutputStream;
6
import java.io.IOException;
7
import java.io.RandomAccessFile;
8
import java.net.URI;
9
import java.nio.ByteBuffer;
10
import java.nio.channels.FileChannel;
11
import java.nio.charset.StandardCharsets;
12
import java.nio.file.Files;
13
import java.nio.file.Path;
14
import java.util.*;
15
import java.util.zip.GZIPOutputStream;
16
17
import static com.renomad.minum.web.StatusLine.StatusCode.CODE_200_OK;
18
import static com.renomad.minum.web.StatusLine.StatusCode.CODE_206_PARTIAL_CONTENT;
19
20
/**
21
 * Represents an HTTP response. This is what will get sent back to the
22
 * client (that is, to the browser).  There are a variety of overloads
23
 * of this record for different situations.  The overarching paradigm is
24
 * to provide you high flexibility.
25
 * <p>
26
 * A response message is sent by a server to a client as a reply to its former request message.
27
 * </p>
28
 * <h3>
29
 * Response syntax
30
 * </h3>
31
 * <p>
32
 * A server sends response messages to the client, which consist of:
33
 * </p>
34
 * <ul>
35
 * <li>
36
 * a status line, consisting of the protocol version, a space, the
37
 * response status code, another space, a possibly empty reason
38
 * phrase, a carriage return and a line feed, e.g.:
39
 * <pre>
40
 * HTTP/1.1 200 OK
41
 * </pre>
42
 * </li>
43
 *
44
 * <li>
45
 * zero or more response header fields, each consisting of the case-insensitive
46
 * field name, a colon, optional leading whitespace, the field value, an
47
 * optional trailing whitespace and ending with a carriage return and a line feed, e.g.:
48
 * <pre>
49
 * Content-Type: text/html
50
 * </pre>
51
 * </li>
52
 *
53
 * <li>
54
 * an empty line, consisting of a carriage return and a line feed;
55
 * </li>
56
 *
57
 * <li>
58
 * an optional message body.
59
 * </li>
60
 *</ul>
61
62
 */
63
public final class Response implements IResponse {
64
65
    private final StatusLine.StatusCode statusCode;
66
    private final Headers extraHeaders;
67
    private final byte[] body;
68
    private final ThrowingConsumer<ISocketWrapper> outputGenerator;
69
    private final long bodyLength;
70
71
    /**
72
     * If true, the body is text, and therefore would probably benefit
73
     * from being compressed before sending (presuming sufficiently large
74
     * to make it worth the time for compression)
75
     */
76
    private final boolean isBodyText;
77
78
    /**
79
     * This is the constructor that provides access to all fields.  It is not intended
80
     * to be used from outside this class.
81
     * @param extraHeaders extra headers we want to return with the response.
82
     * @param outputGenerator a {@link ThrowingConsumer} that will use a {@link ISocketWrapper} parameter
83
     *                        to send bytes on the wire back to the client.  See the static factory methods
84
     *                        such as {@link #buildResponse(StatusLine.StatusCode, Map, byte[])} for more details on this.
85
     * @param bodyLength this is used to set the content-length header for the response.  If this is
86
     *                   not provided, we set the header to "transfer-encoding: chunked", or in other words, streaming.
87
     * @param isBodyText If true, the body is text, and therefore would probably benefit
88
     *                   from being compressed before sending (presuming sufficiently large
89
     *                   to make it worth the time for compression)
90
     */
91
    Response(StatusLine.StatusCode statusCode, Headers extraHeaders, byte[] body,
92
             ThrowingConsumer<ISocketWrapper> outputGenerator, long bodyLength, boolean isBodyText) {
93 1 1. <init> : negated conditional → KILLED
        if (statusCode == null) throw new IllegalArgumentException("Status code must not be null");
94 1 1. <init> : negated conditional → KILLED
        if (extraHeaders == null) throw new IllegalArgumentException("Extra headers must not be null (may use Headers.EMPTY)");
95
        this.statusCode = statusCode;
96
        this.extraHeaders = extraHeaders;
97
        this.body = body;
98
        this.outputGenerator = outputGenerator;
99
        this.bodyLength = bodyLength;
100
        this.isBodyText = isBodyText;
101
    }
102
103
    /**
104
     * This factory method is intended for situations where the user wishes to stream data
105
     * but lacks the content length.  This is only for unusual situations where the developer
106
     * needs the extra control.  In most cases, other methods are more suitable.
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
     */
111
    public static IResponse buildStreamingResponse(StatusLine.StatusCode statusCode, Headers extraHeaders, ThrowingConsumer<ISocketWrapper> outputGenerator) {
112 1 1. buildStreamingResponse : replaced return value with null for com/renomad/minum/web/Response::buildStreamingResponse → KILLED
        return new Response(statusCode, extraHeaders, null, outputGenerator, 0, false);
113
    }
114
115
    /**
116
     * This factory method is intended for situations where the user wishes to stream data
117
     * but lacks the content length.  This is only for unusual situations where the developer
118
     * needs the extra control.  In most cases, other methods are more suitable.
119
     * @param extraHeaders any extra headers for the response, such as the content-type, as a map, where the
120
     *                     key is the header key and the value is the header value. No colon necessary.
121
     * @param outputGenerator a function that will be given a {@link ISocketWrapper}, providing the
122
     *                        ability to send bytes on the socket.
123
     */
124
    public static IResponse buildStreamingResponse(StatusLine.StatusCode statusCode, Map<String,String> extraHeaders, ThrowingConsumer<ISocketWrapper> outputGenerator) {
125 1 1. buildStreamingResponse : replaced return value with null for com/renomad/minum/web/Response::buildStreamingResponse → KILLED
        return buildStreamingResponse(statusCode, convertMapToHeaders(extraHeaders), outputGenerator);
126
    }
127
128
    /**
129
     * Similar to {@link Response#buildStreamingResponse(StatusLine.StatusCode, Headers, ThrowingConsumer)} but here we know
130
     * the body length, so that will be sent to the client as content-length.
131
     * @param extraHeaders any extra headers for the response, such as the content-type
132
     * @param outputGenerator a function that will be given a {@link ISocketWrapper}, providing the
133
     *                        ability to send bytes on the socket.
134
     * @param bodyLength the length, in bytes, of the data to be sent to the client
135
     */
136
    public static IResponse buildStreamingResponse(StatusLine.StatusCode statusCode, Headers extraHeaders, ThrowingConsumer<ISocketWrapper> outputGenerator, long bodyLength) {
137 1 1. buildStreamingResponse : replaced return value with null for com/renomad/minum/web/Response::buildStreamingResponse → KILLED
        return new Response(statusCode, extraHeaders, null, outputGenerator, bodyLength, false);
138
    }
139
140
    /**
141
     * Similar to {@link Response#buildStreamingResponse(StatusLine.StatusCode, Headers, ThrowingConsumer)} but here we know
142
     * the body length, so that will be sent to the client as content-length.
143
     * @param extraHeaders any extra headers for the response, such as the content-type, as a map, where the
144
     *                     key is the header key and the value is the header value. No colon necessary.
145
     * @param outputGenerator a function that will be given a {@link ISocketWrapper}, providing the
146
     *                        ability to send bytes on the socket.
147
     * @param bodyLength the length, in bytes, of the data to be sent to the client
148
     */
149
    public static IResponse buildStreamingResponse(StatusLine.StatusCode statusCode, Map<String,String> extraHeaders, ThrowingConsumer<ISocketWrapper> outputGenerator, long bodyLength) {
150 1 1. buildStreamingResponse : replaced return value with null for com/renomad/minum/web/Response::buildStreamingResponse → KILLED
        return buildStreamingResponse(statusCode, convertMapToHeaders(extraHeaders), outputGenerator, bodyLength);
151
    }
152
153
    /**
154
     * A constructor for situations where the developer wishes to send a small (less than a megabyte) byte array
155
     * to the client.  If there is need to send something of larger size, choose one these
156
     * alternate constructors:
157
     * FileChannel - for sending a large file: {@link Response#buildLargeFileResponse(Headers, String, Headers)}
158
     * Streaming - for more custom streaming control with a known body size: {@link Response#buildStreamingResponse(StatusLine.StatusCode, Map, ThrowingConsumer, long)}
159
     * Streaming - for more custom streaming control with body size unknown: {@link Response#buildStreamingResponse(StatusLine.StatusCode, Map, ThrowingConsumer)}
160
     *
161
     * @param extraHeaders any extra headers for the response, such as the content-type.
162
     */
163
    public static IResponse buildResponse(StatusLine.StatusCode statusCode, Headers extraHeaders, byte[] body) {
164 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, false);
165
    }
166
167
    /**
168
     * A constructor for situations where the developer wishes to send a small (less than a megabyte) byte array
169
     * to the client.  If there is need to send something of larger size, choose one these
170
     * alternate constructors:
171
     * FileChannel - for sending a large file: {@link Response#buildLargeFileResponse(Headers, String, Headers)}
172
     * Streaming - for more custom streaming control with a known body size: {@link Response#buildStreamingResponse(StatusLine.StatusCode, Headers, ThrowingConsumer, long)}
173
     * Streaming - for more custom streaming control with body size unknown: {@link Response#buildStreamingResponse(StatusLine.StatusCode, Headers, ThrowingConsumer)}
174
     *
175
     * @param extraHeaders any extra headers for the response, such as the content-type, as a map, where the
176
     *                     key is the header key and the value is the header value. No colon necessary.
177
     */
178
    public static IResponse buildResponse(StatusLine.StatusCode statusCode, Map<String,String> extraHeaders, byte[] body) {
179 1 1. buildResponse : replaced return value with null for com/renomad/minum/web/Response::buildResponse → KILLED
        return buildResponse(statusCode, convertMapToHeaders(extraHeaders), body);
180
    }
181
182
    private static Headers convertMapToHeaders(Map<String, String> extraHeaders) {
183 2 1. convertMapToHeaders : replaced return value with null for com/renomad/minum/web/Response::convertMapToHeaders → KILLED
2. convertMapToHeaders : negated conditional → KILLED
        if (extraHeaders.isEmpty()) return Headers.EMPTY;
184 2 1. convertMapToHeaders : replaced return value with null for com/renomad/minum/web/Response::convertMapToHeaders → KILLED
2. lambda$convertMapToHeaders$1 : replaced return value with "" for com/renomad/minum/web/Response::lambda$convertMapToHeaders$1 → KILLED
        return new Headers(extraHeaders.entrySet().stream().map(x -> x.getKey() + ": " + x.getValue()).toList());
185
    }
186
187
    /**
188
     * Build an ordinary response, with a known body
189
     * @param extraHeaders extra HTTP headers, like <pre>content-type: text/html</pre>
190
     */
191
    public static IResponse buildResponse(StatusLine.StatusCode statusCode, Headers extraHeaders, String body) {
192
        byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
193 2 1. buildResponse : replaced return value with null for com/renomad/minum/web/Response::buildResponse → KILLED
2. lambda$buildResponse$2 : removed call to com/renomad/minum/web/Response::sendByteArrayResponse → KILLED
        return new Response(statusCode, extraHeaders, bytes, socketWrapper -> sendByteArrayResponse(socketWrapper, bytes), bytes.length, true);
194
    }
195
196
    /**
197
     * Build an ordinary response, with a known body
198
     * @param extraHeaders any extra headers for the response, such as the content-type, as a map, where the
199
     *                     key is the header key and the value is the header value. No colon necessary.
200
     */
201
    public static IResponse buildResponse(StatusLine.StatusCode statusCode, Map<String,String> extraHeaders, String body) {
202 1 1. buildResponse : replaced return value with null for com/renomad/minum/web/Response::buildResponse → KILLED
        return buildResponse(statusCode, convertMapToHeaders(extraHeaders), body);
203
    }
204
205
206
    /**
207
     * This will send a large file to the client as a stream.
208
     * <em>Note that this does not check that the requested file is within a directory. If you expect the
209
     * path value to be untrusted - that is, set by a user - use {@link #buildLargeFileResponse(Headers, String, String, Headers)}</em>
210
     * @param extraHeaders any extra headers for the response, such as the content-type, as a map, where the
211
     *                     key is the header key and the value is the header value. No colon necessary.
212
     *                     For example "Content-Type: video/mp4" or "Content-Type: application/zip"
213
     * @param filePath the string value of the path to this file. <em>Caution! if this is set by the user.  See notes above</em>
214
     * @param requestHeaders these are the headers from the request, which is needed here to set proper
215
     *                       response headers in some cases.
216
     */
217
    public static IResponse buildLargeFileResponse(Map<String,String> extraHeaders, String filePath, Headers requestHeaders) throws IOException {
218 1 1. buildLargeFileResponse : replaced return value with null for com/renomad/minum/web/Response::buildLargeFileResponse → KILLED
        return buildLargeFileResponse(convertMapToHeaders(extraHeaders), filePath, requestHeaders);
219
    }
220
221
    /**
222
     * This will send a large file to the client as a stream.
223
     * <em>Note that this does not check that the requested file is within a directory. If you expect the
224
     * path value to be untrusted - that is, set by a user - use {@link #buildLargeFileResponse(Headers, String, String, Headers)}</em>
225
     * @param extraHeaders extra headers you would like in the response, as key-value pairs in a map - for
226
     *                     example "Content-Type: video/mp4" or "Content-Type: application/zip"
227
     * @param filePath the string value of the path to this file. <em>Caution! if this is set by the user.  See notes above</em>
228
     * @param requestHeaders these are the headers from the request, which is needed here to set proper
229
     *                       response headers in some cases.
230
     */
231
    public static IResponse buildLargeFileResponse(Headers extraHeaders, String filePath, Headers requestHeaders) throws IOException {
232
        Headers adjustedHeaders = extraHeaders;
233
        long fileSize = Files.size(Path.of(filePath));
234
        var range = new Range(requestHeaders, fileSize);
235
        StatusLine.StatusCode responseCode = CODE_200_OK;
236
        long length = fileSize;
237 1 1. buildLargeFileResponse : negated conditional → KILLED
        if (range.hasRangeHeader()) {
238
            long offset = range.getOffset();
239
            length = range.getLength();
240 2 1. buildLargeFileResponse : Replaced long subtraction with addition → TIMED_OUT
2. buildLargeFileResponse : Replaced long addition with subtraction → KILLED
            var lastIndex = (offset + length) - 1;
241
            List<String> headerStrings = new ArrayList<>(extraHeaders.getHeaderStrings());
242
            headerStrings.add("Content-Range: " + String.format("bytes %d-%d/%d", offset, lastIndex, fileSize));
243
            adjustedHeaders = new Headers(headerStrings);
244
            responseCode = CODE_206_PARTIAL_CONTENT;
245
        }
246
247
        ThrowingConsumer<ISocketWrapper> outputGenerator = socketWrapper -> {
248
            try (RandomAccessFile reader = new RandomAccessFile(filePath, "r")) {
249 1 1. lambda$buildLargeFileResponse$3 : removed call to java/io/RandomAccessFile::seek → TIMED_OUT
                reader.seek(range.getOffset());
250
                var fileChannel = reader.getChannel();
251 1 1. lambda$buildLargeFileResponse$3 : removed call to com/renomad/minum/web/Response::sendFileChannelResponse → KILLED
                sendFileChannelResponse(socketWrapper, fileChannel, range.getLength());
252
            }
253
        };
254
255 1 1. buildLargeFileResponse : replaced return value with null for com/renomad/minum/web/Response::buildLargeFileResponse → TIMED_OUT
        return new Response(responseCode, adjustedHeaders, null, outputGenerator, length, false);
256
    }
257
258
    /**
259
     * This will send a large file to the client as a stream.
260
     * This is a safe version of the method, for when the file path is set by a user, meaning it is untrusted.
261
     * By setting a parentDirectory, the system will ensure that the requested path is within that directory, thus
262
     * disallowing path strings that could escape it, like prefixing with a slash.
263
     * @param extraHeaders extra headers you would like in the response, as key-value pairs in a map - for
264
     *                     example "Content-Type: video/mp4" or "Content-Type: application/zip"
265
     * @param filePath the string value of the path to this file. <em>Caution! if this is set by the user.  See notes above</em>
266
     * @param requestHeaders these are the headers from the request, which is needed here to set proper
267
     *                       response headers in some cases.
268
     * @param parentDirectory this value is presumed to be set by the developer, and thus isn't checked for safety. This
269
     *                        allows the developer to use directories anywhere in the system.
270
     */
271
    public static IResponse buildLargeFileResponse(Headers extraHeaders, String filePath, String parentDirectory, Headers requestHeaders) throws IOException {
272
        Path path = FileUtils.safeResolve(parentDirectory, filePath);
273 1 1. buildLargeFileResponse : replaced return value with null for com/renomad/minum/web/Response::buildLargeFileResponse → SURVIVED
        return buildLargeFileResponse(extraHeaders, path.toString(), requestHeaders);
274
    }
275
276
    /**
277
     * This will send a large file to the client as a stream.
278
     * This is a safe version of the method, for when the file path is set by a user, meaning it is untrusted.
279
     * By setting a parentDirectory, the system will ensure that the requested path is within that directory, thus
280
     * disallowing path strings that could escape it, like prefixing with a slash.
281
     * @param extraHeaders any extra headers for the response, such as the content-type, as a map, where the
282
     *                     key is the header key and the value is the header value. No colon necessary.
283
     *                     For example "Content-Type: video/mp4" or "Content-Type: application/zip"
284
     * @param filePath the string value of the path to this file. <em>Caution! if this is set by the user.  See notes above</em>
285
     * @param requestHeaders these are the headers from the request, which is needed here to set proper
286
     *                       response headers in some cases.
287
     * @param parentDirectory this value is presumed to be set by the developer, and thus isn't checked for safety. This
288
     *                        allows the developer to use directories anywhere in the system.
289
     */
290
    public static IResponse buildLargeFileResponse(Map<String,String> extraHeaders, String filePath, String parentDirectory, Headers requestHeaders) throws IOException {
291 1 1. buildLargeFileResponse : replaced return value with null for com/renomad/minum/web/Response::buildLargeFileResponse → SURVIVED
        return buildLargeFileResponse(convertMapToHeaders(extraHeaders), filePath, parentDirectory, requestHeaders);
292
    }
293
294
    /**
295
     * Build a {@link Response} with just a status code and headers, without a body
296
     * @param extraHeaders extra HTTP headers
297
     */
298
    public static IResponse buildLeanResponse(StatusLine.StatusCode statusCode, Headers extraHeaders) {
299 1 1. buildLeanResponse : replaced return value with null for com/renomad/minum/web/Response::buildLeanResponse → TIMED_OUT
        return new Response(statusCode, extraHeaders, null, socketWrapper -> {}, 0, false);
300
    }
301
302
    /**
303
     * Build a {@link Response} with just a status code and headers, without a body
304
     * @param extraHeaders any extra headers for the response, such as the content-type, as a map, where the
305
     *                     key is the header key and the value is the header value. No colon necessary.
306
     */
307
    public static IResponse buildLeanResponse(StatusLine.StatusCode statusCode, Map<String,String> extraHeaders) {
308 1 1. buildLeanResponse : replaced return value with null for com/renomad/minum/web/Response::buildLeanResponse → KILLED
        return buildLeanResponse(statusCode, convertMapToHeaders(extraHeaders));
309
    }
310
311
    /**
312
     * Build a {@link Response} with only a status code, with no body and no extra headers.
313
     */
314
    public static IResponse buildLeanResponse(StatusLine.StatusCode statusCode) {
315 1 1. buildLeanResponse : replaced return value with null for com/renomad/minum/web/Response::buildLeanResponse → KILLED
        return new Response(statusCode, Headers.EMPTY, null, socketWrapper -> {}, 0, false);
316
    }
317
318
    /**
319
     * A helper method to create a response that returns a
320
     * 303 status code ("see other").  Provide a url that will
321
     * be handed to the browser.  This url may be relative or absolute.
322
     */
323
    public static IResponse redirectTo(String locationUrl) {
324
        try {
325
            // check if we can create a URL from this - any failures indicate invalid syntax.
326
            URI.create(locationUrl);
327
        } catch (Exception ex) {
328
            throw new WebServerException("Failure in redirect to (" + locationUrl + "). Exception: " + ex);
329
        }
330 1 1. redirectTo : replaced return value with null for com/renomad/minum/web/Response::redirectTo → KILLED
        return buildResponse(StatusLine.StatusCode.CODE_303_SEE_OTHER, new Headers(List.of("location: " + locationUrl, "Content-Type: text/html; charset=UTF-8")), "<p>See <a href=\""+locationUrl+"\">this link</a></p>");
331
    }
332
333
    /**
334
     * If you are returning HTML text with a 200 ok, this is a helper that
335
     * lets you skip some of the boilerplate. This version of the helper
336
     * lets you add extra headers on top of the basic content-type headers
337
     * that are needed to specify this is HTML.
338
     */
339
    public static IResponse htmlOk(String body, Headers extraHeaders) {
340
        List<String> headersList = new ArrayList<>(extraHeaders.getHeaderStrings());
341
        headersList.add("Content-Type: text/html; charset=UTF-8");
342
        var headers =  new Headers(headersList);
343 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);
344
    }
345
346
    /**
347
     * If you are returning HTML text with a 200 ok, this is a helper that
348
     * lets you skip some of the boilerplate.
349
     */
350
    public static IResponse htmlOk(String body) {
351 1 1. htmlOk : replaced return value with null for com/renomad/minum/web/Response::htmlOk → KILLED
        return htmlOk(body, Headers.EMPTY);
352
    }
353
354
    @Override
355
    public Headers getExtraHeaders() {
356 1 1. getExtraHeaders : replaced return value with null for com/renomad/minum/web/Response::getExtraHeaders → KILLED
        return new Headers(this.extraHeaders.getHeaderStrings());
357
    }
358
359
    @Override
360
    public StatusLine.StatusCode getStatusCode() {
361 1 1. getStatusCode : replaced return value with null for com/renomad/minum/web/Response::getStatusCode → KILLED
        return statusCode;
362
    }
363
364
    @Override
365
    public boolean isBodyText() {
366 2 1. isBodyText : replaced boolean return with true for com/renomad/minum/web/Response::isBodyText → KILLED
2. isBodyText : replaced boolean return with false for com/renomad/minum/web/Response::isBodyText → KILLED
        return isBodyText;
367
    }
368
369
    @Override
370
    public long getBodyLength() {
371 1 1. getBodyLength : negated conditional → TIMED_OUT
        if (body != null) {
372 1 1. getBodyLength : replaced long return with 0 for com/renomad/minum/web/Response::getBodyLength → KILLED
            return body.length;
373
        } else {
374 1 1. getBodyLength : replaced long return with 0 for com/renomad/minum/web/Response::getBodyLength → KILLED
            return bodyLength;
375
        }
376
    }
377
378
    @Override
379
    public void sendBody(ISocketWrapper sw) throws IOException {
380
        try {
381 1 1. sendBody : removed call to com/renomad/minum/web/ThrowingConsumer::accept → KILLED
            outputGenerator.accept(sw);
382
        } catch (Exception ex) {
383
            throw new IOException(ex.getMessage(), ex);
384
        }
385
    }
386
387
    /**
388
     * put bytes from a file into the socket, sending to the client
389
     * @param fileChannel the file we are reading from, based on a {@link RandomAccessFile}
390
     * @param length the number of bytes to send.  May be less than the full length of this {@link FileChannel}
391
     */
392
    private static void sendFileChannelResponse(ISocketWrapper sw, FileChannel fileChannel, long length) throws IOException {
393
        try {
394
            int bufferSize = 8 * 1024;
395
            ByteBuffer buff = ByteBuffer.allocate(bufferSize);
396
            long countBytesLeftToSend = length;
397
            while (true) {
398
                int countBytesRead = fileChannel.read(buff);
399 2 1. sendFileChannelResponse : negated conditional → TIMED_OUT
2. sendFileChannelResponse : changed conditional boundary → KILLED
                if (countBytesRead <= 0) {
400
                    break;
401
                } else {
402 2 1. sendFileChannelResponse : negated conditional → KILLED
2. sendFileChannelResponse : changed conditional boundary → KILLED
                    if (countBytesLeftToSend < countBytesRead) {
403 1 1. sendFileChannelResponse : removed call to com/renomad/minum/web/ISocketWrapper::send → TIMED_OUT
                        sw.send(buff.array(), 0, (int)countBytesLeftToSend);
404
                        break;
405
                    } else {
406 1 1. sendFileChannelResponse : removed call to com/renomad/minum/web/ISocketWrapper::send → KILLED
                        sw.send(buff.array(), 0, countBytesRead);
407
                    }
408
                    buff.clear();
409
                }
410 1 1. sendFileChannelResponse : Replaced long subtraction with addition → KILLED
                countBytesLeftToSend -= countBytesRead;
411
            }
412
        } finally {
413 1 1. sendFileChannelResponse : removed call to java/nio/channels/FileChannel::close → KILLED
            fileChannel.close();
414
        }
415
    }
416
417
    private static void sendByteArrayResponse(ISocketWrapper sw, byte[] body) throws IOException {
418 1 1. sendByteArrayResponse : removed call to com/renomad/minum/web/ISocketWrapper::send → KILLED
        sw.send(body);
419
    }
420
421
    @Override
422
    public byte[] getBody() {
423 1 1. getBody : replaced return value with null for com/renomad/minum/web/Response::getBody → KILLED
        return body;
424
    }
425
426
    /**
427
     * Compress the data in this body using gzip.
428
     * <br>
429
     * This operates by getting the body field from this instance of {@link Response} and
430
     * creating a new Response with the compressed data.
431
     */
432
    Response compressBody() throws IOException {
433
434
        ByteArrayOutputStream out = new ByteArrayOutputStream();
435
        var gos = new GZIPOutputStream(out);
436 1 1. compressBody : removed call to java/util/zip/GZIPOutputStream::write → SURVIVED
        gos.write(body);
437 1 1. compressBody : removed call to java/util/zip/GZIPOutputStream::finish → SURVIVED
        gos.finish();
438 1 1. compressBody : replaced return value with null for com/renomad/minum/web/Response::compressBody → KILLED
        return (Response)Response.buildResponse(
439
                statusCode,
440
                extraHeaders,
441
                out.toByteArray()
442
        );
443
    }
444
445
    @Override
446
    public boolean equals(Object o) {
447 2 1. equals : replaced boolean return with false for com/renomad/minum/web/Response::equals → TIMED_OUT
2. equals : negated conditional → KILLED
        if (this == o) return true;
448 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;
449
        Response response = (Response) o;
450 6 1. equals : replaced boolean return with true for com/renomad/minum/web/Response::equals → KILLED
2. equals : negated conditional → KILLED
3. equals : negated conditional → KILLED
4. equals : negated conditional → KILLED
5. equals : negated conditional → KILLED
6. equals : negated conditional → KILLED
        return bodyLength == response.bodyLength && isBodyText == response.isBodyText && statusCode == response.statusCode && Objects.equals(extraHeaders, response.extraHeaders) && Arrays.equals(body, response.body);
451
    }
452
453
    @Override
454
    public int hashCode() {
455
        int result = Objects.hash(statusCode, extraHeaders, bodyLength, isBodyText);
456 2 1. hashCode : Replaced integer addition with subtraction → TIMED_OUT
2. hashCode : Replaced integer multiplication with division → TIMED_OUT
        result = 31 * result + Arrays.hashCode(body);
457 1 1. hashCode : replaced int return with 0 for com/renomad/minum/web/Response::hashCode → TIMED_OUT
        return result;
458
    }
459
460
    @Override
461
    public String toString() {
462 1 1. toString : replaced return value with "" for com/renomad/minum/web/Response::toString → KILLED
        return "Response{" +
463
                "statusCode=" + statusCode +
464
                ", extraHeaders=" + extraHeaders +
465
                ", bodyLength=" + bodyLength +
466
                ", isBodyText=" + isBodyText +
467
                '}';
468
    }
469
}

Mutations

93

1.1
Location : <init>
Killed by : com.renomad.minum.web.ResponseTests.testNullStatusCode(com.renomad.minum.web.ResponseTests)
negated conditional → KILLED

94

1.1
Location : <init>
Killed by : com.renomad.minum.web.ResponseTests.testNullExtraHeaders(com.renomad.minum.web.ResponseTests)
negated conditional → KILLED

112

1.1
Location : buildStreamingResponse
Killed by : com.renomad.minum.web.ResponseTests.testResponse_Streaming(com.renomad.minum.web.ResponseTests)
replaced return value with null for com/renomad/minum/web/Response::buildStreamingResponse → KILLED

125

1.1
Location : buildStreamingResponse
Killed by : com.renomad.minum.web.ResponseTests.testResponse_Streaming(com.renomad.minum.web.ResponseTests)
replaced return value with null for com/renomad/minum/web/Response::buildStreamingResponse → KILLED

137

1.1
Location : buildStreamingResponse
Killed by : com.renomad.minum.web.WebTests
replaced return value with null for com/renomad/minum/web/Response::buildStreamingResponse → KILLED

150

1.1
Location : buildStreamingResponse
Killed by : com.renomad.minum.web.WebTests
replaced return value with null for com/renomad/minum/web/Response::buildStreamingResponse → KILLED

164

1.1
Location : lambda$buildResponse$0
Killed by : com.renomad.minum.FunctionalTests.testEndToEnd_Functional(com.renomad.minum.FunctionalTests)
removed call to com/renomad/minum/web/Response::sendByteArrayResponse → KILLED

2.2
Location : buildResponse
Killed by : com.renomad.minum.web.WebFrameworkTests.test_compressIfRequested(com.renomad.minum.web.WebFrameworkTests)
replaced return value with null for com/renomad/minum/web/Response::buildResponse → KILLED

179

1.1
Location : buildResponse
Killed by : com.renomad.minum.FunctionalTests.testEndToEnd_Functional(com.renomad.minum.FunctionalTests)
replaced return value with null for com/renomad/minum/web/Response::buildResponse → KILLED

183

1.1
Location : convertMapToHeaders
Killed by : com.renomad.minum.web.ResponseTests.testResponse_Streaming(com.renomad.minum.web.ResponseTests)
replaced return value with null for com/renomad/minum/web/Response::convertMapToHeaders → KILLED

2.2
Location : convertMapToHeaders
Killed by : com.renomad.minum.FunctionalTests.test_PathFunction_Response(com.renomad.minum.FunctionalTests)
negated conditional → KILLED

184

1.1
Location : convertMapToHeaders
Killed by : com.renomad.minum.web.WebFrameworkTests.test_compressIfRequested(com.renomad.minum.web.WebFrameworkTests)
replaced return value with null for com/renomad/minum/web/Response::convertMapToHeaders → KILLED

2.2
Location : lambda$convertMapToHeaders$1
Killed by : com.renomad.minum.FunctionalTests.test_PathFunction_Response(com.renomad.minum.FunctionalTests)
replaced return value with "" for com/renomad/minum/web/Response::lambda$convertMapToHeaders$1 → KILLED

193

1.1
Location : buildResponse
Killed by : com.renomad.minum.web.ResponseTests.testResponse_EdgeCase_SendBodyWithException(com.renomad.minum.web.ResponseTests)
replaced return value with null for com/renomad/minum/web/Response::buildResponse → KILLED

2.2
Location : lambda$buildResponse$2
Killed by : com.renomad.minum.web.ResponseTests.testResponse_EdgeCase_SendBodyWithException(com.renomad.minum.web.ResponseTests)
removed call to com/renomad/minum/web/Response::sendByteArrayResponse → KILLED

202

1.1
Location : buildResponse
Killed by : com.renomad.minum.web.ResponseTests.testResponse_EdgeCase_SendBodyWithException(com.renomad.minum.web.ResponseTests)
replaced return value with null for com/renomad/minum/web/Response::buildResponse → KILLED

218

1.1
Location : buildLargeFileResponse
Killed by : com.renomad.minum.FunctionalTests.testEndToEnd_Functional(com.renomad.minum.FunctionalTests)
replaced return value with null for com/renomad/minum/web/Response::buildLargeFileResponse → KILLED

237

1.1
Location : buildLargeFileResponse
Killed by : com.renomad.minum.FunctionalTests.testEndToEnd_Functional(com.renomad.minum.FunctionalTests)
negated conditional → KILLED

240

1.1
Location : buildLargeFileResponse
Killed by : com.renomad.minum.FunctionalTests.testEndToEnd_Functional(com.renomad.minum.FunctionalTests)
Replaced long addition with subtraction → KILLED

2.2
Location : buildLargeFileResponse
Killed by : none
Replaced long subtraction with addition → TIMED_OUT

249

1.1
Location : lambda$buildLargeFileResponse$3
Killed by : none
removed call to java/io/RandomAccessFile::seek → TIMED_OUT

251

1.1
Location : lambda$buildLargeFileResponse$3
Killed by : com.renomad.minum.FunctionalTests.testEndToEnd_Functional(com.renomad.minum.FunctionalTests)
removed call to com/renomad/minum/web/Response::sendFileChannelResponse → KILLED

255

1.1
Location : buildLargeFileResponse
Killed by : none
replaced return value with null for com/renomad/minum/web/Response::buildLargeFileResponse → TIMED_OUT

273

1.1
Location : buildLargeFileResponse
Killed by : none
replaced return value with null for com/renomad/minum/web/Response::buildLargeFileResponse → SURVIVED
Covering tests

291

1.1
Location : buildLargeFileResponse
Killed by : none
replaced return value with null for com/renomad/minum/web/Response::buildLargeFileResponse → SURVIVED
Covering tests

299

1.1
Location : buildLeanResponse
Killed by : none
replaced return value with null for com/renomad/minum/web/Response::buildLeanResponse → TIMED_OUT

308

1.1
Location : buildLeanResponse
Killed by : com.renomad.minum.FunctionalTests.testEndToEnd_Functional(com.renomad.minum.FunctionalTests)
replaced return value with null for com/renomad/minum/web/Response::buildLeanResponse → KILLED

315

1.1
Location : buildLeanResponse
Killed by : com.renomad.minum.web.WebFrameworkTests.test_readStaticFile_Edge_Colon(com.renomad.minum.web.WebFrameworkTests)
replaced return value with null for com/renomad/minum/web/Response::buildLeanResponse → KILLED

330

1.1
Location : redirectTo
Killed by : com.renomad.minum.FunctionalTests.testEndToEnd_Functional(com.renomad.minum.FunctionalTests)
replaced return value with null for com/renomad/minum/web/Response::redirectTo → KILLED

343

1.1
Location : htmlOk
Killed by : com.renomad.minum.web.ResponseTests.testToString(com.renomad.minum.web.ResponseTests)
replaced return value with null for com/renomad/minum/web/Response::htmlOk → KILLED

351

1.1
Location : htmlOk
Killed by : com.renomad.minum.web.ResponseTests.testToString(com.renomad.minum.web.ResponseTests)
replaced return value with null for com/renomad/minum/web/Response::htmlOk → KILLED

356

1.1
Location : getExtraHeaders
Killed by : com.renomad.minum.web.WebFrameworkTests.test_readStaticFile_JS(com.renomad.minum.web.WebFrameworkTests)
replaced return value with null for com/renomad/minum/web/Response::getExtraHeaders → KILLED

361

1.1
Location : getStatusCode
Killed by : com.renomad.minum.web.WebFrameworkTests.test_readStaticFile_Edge_Colon(com.renomad.minum.web.WebFrameworkTests)
replaced return value with null for com/renomad/minum/web/Response::getStatusCode → KILLED

366

1.1
Location : isBodyText
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
replaced boolean return with true for com/renomad/minum/web/Response::isBodyText → KILLED

2.2
Location : isBodyText
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
replaced boolean return with false for com/renomad/minum/web/Response::isBodyText → KILLED

371

1.1
Location : getBodyLength
Killed by : none
negated conditional → TIMED_OUT

372

1.1
Location : getBodyLength
Killed by : com.renomad.minum.web.WebFrameworkTests.test_compressIfRequested(com.renomad.minum.web.WebFrameworkTests)
replaced long return with 0 for com/renomad/minum/web/Response::getBodyLength → KILLED

374

1.1
Location : getBodyLength
Killed by : com.renomad.minum.web.WebTests
replaced long return with 0 for com/renomad/minum/web/Response::getBodyLength → KILLED

381

1.1
Location : sendBody
Killed by : com.renomad.minum.web.ResponseTests.testResponse_Streaming(com.renomad.minum.web.ResponseTests)
removed call to com/renomad/minum/web/ThrowingConsumer::accept → KILLED

399

1.1
Location : sendFileChannelResponse
Killed by : com.renomad.minum.FunctionalTests.testEndToEnd_Functional(com.renomad.minum.FunctionalTests)
changed conditional boundary → KILLED

2.2
Location : sendFileChannelResponse
Killed by : none
negated conditional → TIMED_OUT

402

1.1
Location : sendFileChannelResponse
Killed by : com.renomad.minum.FunctionalTests.testEndToEnd_Functional(com.renomad.minum.FunctionalTests)
negated conditional → KILLED

2.2
Location : sendFileChannelResponse
Killed by : com.renomad.minum.FunctionalTests.testEndToEnd_Functional(com.renomad.minum.FunctionalTests)
changed conditional boundary → KILLED

403

1.1
Location : sendFileChannelResponse
Killed by : none
removed call to com/renomad/minum/web/ISocketWrapper::send → TIMED_OUT

406

1.1
Location : sendFileChannelResponse
Killed by : com.renomad.minum.FunctionalTests.testEndToEnd_Functional(com.renomad.minum.FunctionalTests)
removed call to com/renomad/minum/web/ISocketWrapper::send → KILLED

410

1.1
Location : sendFileChannelResponse
Killed by : com.renomad.minum.FunctionalTests.testEndToEnd_Functional(com.renomad.minum.FunctionalTests)
Replaced long subtraction with addition → KILLED

413

1.1
Location : sendFileChannelResponse
Killed by : com.renomad.minum.FunctionalTests.testEndToEnd_Functional(com.renomad.minum.FunctionalTests)
removed call to java/nio/channels/FileChannel::close → KILLED

418

1.1
Location : sendByteArrayResponse
Killed by : com.renomad.minum.web.ResponseTests.testResponse_EdgeCase_SendBodyWithException(com.renomad.minum.web.ResponseTests)
removed call to com/renomad/minum/web/ISocketWrapper::send → KILLED

423

1.1
Location : getBody
Killed by : com.renomad.minum.web.EndpointTests.test_Endpoint_HappyPath(com.renomad.minum.web.EndpointTests)
replaced return value with null for com/renomad/minum/web/Response::getBody → KILLED

436

1.1
Location : compressBody
Killed by : none
removed call to java/util/zip/GZIPOutputStream::write → SURVIVED
Covering tests

437

1.1
Location : compressBody
Killed by : none
removed call to java/util/zip/GZIPOutputStream::finish → SURVIVED
Covering tests

438

1.1
Location : compressBody
Killed by : com.renomad.minum.web.WebFrameworkTests.test_compressIfRequested(com.renomad.minum.web.WebFrameworkTests)
replaced return value with null for com/renomad/minum/web/Response::compressBody → KILLED

447

1.1
Location : equals
Killed by : none
replaced boolean return with false for com/renomad/minum/web/Response::equals → TIMED_OUT

2.2
Location : equals
Killed by : com.renomad.minum.web.WebTests
negated conditional → KILLED

448

1.1
Location : equals
Killed by : com.renomad.minum.web.ResponseTests.testUseResponseAsKey(com.renomad.minum.web.ResponseTests)
negated conditional → KILLED

2.2
Location : equals
Killed by : com.renomad.minum.EqualsTests.equalsTest(com.renomad.minum.EqualsTests)
replaced boolean return with true for com/renomad/minum/web/Response::equals → KILLED

3.3
Location : equals
Killed by : com.renomad.minum.web.ResponseTests.testUseResponseAsKey(com.renomad.minum.web.ResponseTests)
negated conditional → KILLED

450

1.1
Location : equals
Killed by : com.renomad.minum.web.WebTests
replaced boolean return with true for com/renomad/minum/web/Response::equals → KILLED

2.2
Location : equals
Killed by : com.renomad.minum.web.ResponseTests.testUseResponseAsKey(com.renomad.minum.web.ResponseTests)
negated conditional → KILLED

3.3
Location : equals
Killed by : com.renomad.minum.web.ResponseTests.testUseResponseAsKey(com.renomad.minum.web.ResponseTests)
negated conditional → KILLED

4.4
Location : equals
Killed by : com.renomad.minum.web.ResponseTests.testUseResponseAsKey(com.renomad.minum.web.ResponseTests)
negated conditional → KILLED

5.5
Location : equals
Killed by : com.renomad.minum.web.ResponseTests.testUseResponseAsKey(com.renomad.minum.web.ResponseTests)
negated conditional → KILLED

6.6
Location : equals
Killed by : com.renomad.minum.web.ResponseTests.testUseResponseAsKey(com.renomad.minum.web.ResponseTests)
negated conditional → KILLED

456

1.1
Location : hashCode
Killed by : none
Replaced integer addition with subtraction → TIMED_OUT

2.2
Location : hashCode
Killed by : none
Replaced integer multiplication with division → TIMED_OUT

457

1.1
Location : hashCode
Killed by : none
replaced int return with 0 for com/renomad/minum/web/Response::hashCode → TIMED_OUT

462

1.1
Location : toString
Killed by : com.renomad.minum.web.ResponseTests.testToString(com.renomad.minum.web.ResponseTests)
replaced return value with "" for com/renomad/minum/web/Response::toString → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0