Headers.java

1
package com.renomad.minum.web;
2
3
import com.renomad.minum.security.ForbiddenUseException;
4
5
import java.io.IOException;
6
import java.io.InputStream;
7
import java.util.*;
8
9
import static com.renomad.minum.utils.Invariants.mustBeTrue;
10
11
/**
12
 * Details extracted from the headers.  For example,
13
 * is this a keep-alive connection? what is the content-length,
14
 * and so on.
15
 * Here is some detail from <a href="https://en.wikipedia.org/wiki/List_of_HTTP_header_fields">Wikipedia</a> on the subject:
16
 * <p>
17
 * HTTP header fields are a list of strings sent and received by both
18
 * the client program and server on every HTTP request and response. These
19
 * headers are usually invisible to the end-user and are only processed or
20
 * logged by the server and client applications. They define how information
21
 * sent/received through the connection are encoded (as in Content-Encoding),
22
 * the session verification and identification of the client (as in browser
23
 * cookies, IP address, user-agent) or their anonymity thereof (VPN or
24
 * proxy masking, user-agent spoofing), how the server should handle data
25
 * (as in Do-Not-Track), the age (the time it has resided in a shared cache)
26
 * of the document being downloaded, amongst others.
27
 * </p>
28
 */
29
public final class Headers{
30
31
    public static final Headers EMPTY = new Headers(List.of());
32
    private static final int MAX_HEADERS_COUNT = 70;
33
34
    /**
35
     * Each line of the headers is read into this data structure
36
     */
37
    private final List<String> headerStrings;
38
    private final Map<String, List<String>> headersMap;
39
40
    public Headers(
41
            List<String> headerStrings
42
    ) {
43
        this.headerStrings = new ArrayList<>(headerStrings);
44
        this.headersMap = Collections.unmodifiableMap(extractHeadersToMap(headerStrings));
45
    }
46
47
    public List<String> getHeaderStrings() {
48 1 1. getHeaderStrings : replaced return value with Collections.emptyList for com/renomad/minum/web/Headers::getHeaderStrings → KILLED
        return new ArrayList<>(headerStrings);
49
    }
50
51
    /**
52
     * Obtain any desired header by looking it up in this map.  All keys
53
     * are made lowercase.
54
     */
55
    static Map<String, List<String>> extractHeadersToMap(List<String> headerStrings) {
56
        var result = new HashMap<String, List<String>>();
57
        for (var h : headerStrings) {
58
            var indexOfFirstColon = h.indexOf(":");
59
60
            // if the header is malformed, just move on
61 2 1. extractHeadersToMap : changed conditional boundary → TIMED_OUT
2. extractHeadersToMap : negated conditional → KILLED
            if (indexOfFirstColon <= 0) continue;
62
63
            String key = h.substring(0, indexOfFirstColon).toLowerCase(Locale.ROOT);
64 1 1. extractHeadersToMap : Replaced integer addition with subtraction → KILLED
            String value = h.substring(indexOfFirstColon+1).trim();
65
66 1 1. extractHeadersToMap : negated conditional → KILLED
            if (result.containsKey(key)) {
67
                var currentValue = result.get(key);
68
                List<String> newList = new ArrayList<>();
69
                newList.add(value);
70
                newList.addAll(currentValue);
71
                result.put(key, newList);
72
            } else {
73
                result.put(key, List.of(value));
74
            }
75
76
        }
77 1 1. extractHeadersToMap : replaced return value with Collections.emptyMap for com/renomad/minum/web/Headers::extractHeadersToMap → KILLED
        return result;
78
    }
79
80
    /**
81
     * Gets the one content-type header, or returns an empty string
82
     */
83
    public String contentType() {
84
        // find the header that starts with content-type
85
        List<String> cts = Objects.requireNonNullElse(headersMap.get("content-type"), List.of());
86 2 1. contentType : changed conditional boundary → KILLED
2. contentType : negated conditional → KILLED
        if (cts.size() > 1) {
87 1 1. contentType : removed call to java/util/List::sort → SURVIVED
            cts.sort(Comparator.naturalOrder());
88
            throw new WebServerException("The number of content-type headers must be exactly zero or one.  Received: " + cts);
89
        }
90 1 1. contentType : negated conditional → KILLED
        if (!cts.isEmpty()) {
91 1 1. contentType : replaced return value with "" for com/renomad/minum/web/Headers::contentType → KILLED
            return cts.getFirst();
92
        }
93
94
        // if we don't find a content-type header, or if we don't find one we can handle, return an empty string.
95
        return "";
96
    }
97
98
    /**
99
     * Given the list of headers, find the one with the length of the
100
     * body of the POST and return that value as an integer. If
101
     * we do not find a content length, return -1.
102
     */
103
    public int contentLength() {
104
        List<String> cl = Objects.requireNonNullElse(headersMap.get("content-length"), List.of());
105 2 1. contentLength : negated conditional → KILLED
2. contentLength : changed conditional boundary → KILLED
        if (cl.size() > 1) {
106 1 1. contentLength : removed call to java/util/List::sort → KILLED
            cl.sort(Comparator.naturalOrder());
107
            throw new WebServerException("The number of content-length headers must be exactly zero or one.  Received: " + cl);
108
        }
109
        int contentLength = -1;
110 1 1. contentLength : negated conditional → KILLED
        if (!cl.isEmpty()) {
111
            contentLength = Integer.parseInt(cl.getFirst());
112
            mustBeTrue(contentLength >= 0, "Content-length cannot be negative");
113
        }
114
115 1 1. contentLength : replaced int return with 0 for com/renomad/minum/web/Headers::contentLength → KILLED
        return contentLength;
116
    }
117
118
    /**
119
     * Indicates whether the headers in this request
120
     * have a Connection: Keep-Alive
121
     */
122
    public boolean hasKeepAlive() {
123
        List<String> connectionHeader = headersMap.get("connection");
124 2 1. hasKeepAlive : negated conditional → KILLED
2. hasKeepAlive : replaced boolean return with true for com/renomad/minum/web/Headers::hasKeepAlive → KILLED
        if (connectionHeader == null) return false;
125 4 1. hasKeepAlive : replaced boolean return with true for com/renomad/minum/web/Headers::hasKeepAlive → KILLED
2. lambda$hasKeepAlive$0 : replaced boolean return with true for com/renomad/minum/web/Headers::lambda$hasKeepAlive$0 → KILLED
3. hasKeepAlive : replaced boolean return with false for com/renomad/minum/web/Headers::hasKeepAlive → KILLED
4. lambda$hasKeepAlive$0 : replaced boolean return with false for com/renomad/minum/web/Headers::lambda$hasKeepAlive$0 → KILLED
        return connectionHeader.stream().anyMatch(x -> x.toLowerCase(Locale.ROOT).contains("keep-alive"));
126
    }
127
128
    /**
129
     * Indicates whether the headers in this request
130
     * have a Connection: close
131
     */
132
    public boolean hasConnectionClose() {
133
        List<String> connectionHeader = headersMap.get("connection");
134 2 1. hasConnectionClose : replaced boolean return with true for com/renomad/minum/web/Headers::hasConnectionClose → KILLED
2. hasConnectionClose : negated conditional → KILLED
        if (connectionHeader == null) return false;
135 4 1. hasConnectionClose : replaced boolean return with false for com/renomad/minum/web/Headers::hasConnectionClose → KILLED
2. hasConnectionClose : replaced boolean return with true for com/renomad/minum/web/Headers::hasConnectionClose → KILLED
3. lambda$hasConnectionClose$1 : replaced boolean return with true for com/renomad/minum/web/Headers::lambda$hasConnectionClose$1 → KILLED
4. lambda$hasConnectionClose$1 : replaced boolean return with false for com/renomad/minum/web/Headers::lambda$hasConnectionClose$1 → KILLED
        return connectionHeader.stream().anyMatch(x -> x.toLowerCase(Locale.ROOT).contains("close"));
136
    }
137
138
    /**
139
     * Loop through the lines of header in the HTTP message
140
     */
141
    static List<String> getAllHeaders(InputStream is, IInputStreamUtils inputStreamUtils) {
142
        // we'll give the list an initial size, since in most cases we're going to have headers.
143
        // 10 is just an arbitrary number, seems about right.
144
        List<String> headers = new ArrayList<>(10);
145 1 1. getAllHeaders : Changed increment from 1 to -1 → TIMED_OUT
        for (int i = 0;; i++) {
146 2 1. getAllHeaders : negated conditional → KILLED
2. getAllHeaders : changed conditional boundary → KILLED
            if (i >=MAX_HEADERS_COUNT) {
147
                throw new ForbiddenUseException("User tried sending too many headers.  max: " + MAX_HEADERS_COUNT);
148
            }
149
            String value;
150
            try {
151
                value = inputStreamUtils.readLine(is);
152
            } catch (IOException e) {
153
                throw new WebServerException(e);
154
            }
155 2 1. getAllHeaders : negated conditional → KILLED
2. getAllHeaders : negated conditional → KILLED
            if (value != null && value.isBlank()) {
156
                break;
157 1 1. getAllHeaders : negated conditional → KILLED
            } else if (value == null) {
158 1 1. getAllHeaders : replaced return value with Collections.emptyList for com/renomad/minum/web/Headers::getAllHeaders → KILLED
                return headers;
159
            } else {
160
                headers.add(value);
161
            }
162
        }
163 1 1. getAllHeaders : replaced return value with Collections.emptyList for com/renomad/minum/web/Headers::getAllHeaders → KILLED
        return headers;
164
    }
165
166
    /**
167
     * Allows a user to obtain any header value by its key, case-insensitively
168
     * @return a {@link List} of string values, or null
169
     * if no header was found.
170
     */
171
    public List<String> valueByKey(String key) {
172 1 1. valueByKey : replaced return value with Collections.emptyList for com/renomad/minum/web/Headers::valueByKey → KILLED
        return headersMap.get(key.toLowerCase(Locale.ROOT));
173
    }
174
175
    @Override
176
    public boolean equals(Object o) {
177 2 1. equals : negated conditional → TIMED_OUT
2. equals : replaced boolean return with false for com/renomad/minum/web/Headers::equals → TIMED_OUT
        if (this == o) return true;
178 3 1. equals : replaced boolean return with true for com/renomad/minum/web/Headers::equals → TIMED_OUT
2. equals : negated conditional → KILLED
3. equals : negated conditional → KILLED
        if (o == null || getClass() != o.getClass()) return false;
179
        Headers headers = (Headers) o;
180 3 1. equals : replaced boolean return with true for com/renomad/minum/web/Headers::equals → TIMED_OUT
2. equals : negated conditional → KILLED
3. equals : negated conditional → KILLED
        return Objects.equals(headerStrings, headers.headerStrings) && Objects.equals(headersMap, headers.headersMap);
181
    }
182
183
    @Override
184
    public int hashCode() {
185 1 1. hashCode : replaced int return with 0 for com/renomad/minum/web/Headers::hashCode → TIMED_OUT
        return Objects.hash(headerStrings, headersMap);
186
    }
187
188
    @Override
189
    public String toString() {
190 1 1. toString : replaced return value with "" for com/renomad/minum/web/Headers::toString → KILLED
        return "Headers{" +
191
                "headerStrings=" + headerStrings +
192
                '}';
193
    }
194
}

Mutations

48

1.1
Location : getHeaderStrings
Killed by : com.renomad.minum.web.WebTests
replaced return value with Collections.emptyList for com/renomad/minum/web/Headers::getHeaderStrings → KILLED

61

1.1
Location : extractHeadersToMap
Killed by : none
changed conditional boundary → TIMED_OUT

2.2
Location : extractHeadersToMap
Killed by : com.renomad.minum.web.RangeTests.test_MultipleRangeHeaders(com.renomad.minum.web.RangeTests)
negated conditional → KILLED

64

1.1
Location : extractHeadersToMap
Killed by : com.renomad.minum.web.RangeTests.test_DetermineLengthFromRangeHeader_EdgeCase_FirstLarger(com.renomad.minum.web.RangeTests)
Replaced integer addition with subtraction → KILLED

66

1.1
Location : extractHeadersToMap
Killed by : com.renomad.minum.web.RangeTests.test_MultipleRangeHeaders(com.renomad.minum.web.RangeTests)
negated conditional → KILLED

77

1.1
Location : extractHeadersToMap
Killed by : com.renomad.minum.web.RangeTests.test_MultipleRangeHeaders(com.renomad.minum.web.RangeTests)
replaced return value with Collections.emptyMap for com/renomad/minum/web/Headers::extractHeadersToMap → KILLED

86

1.1
Location : contentType
Killed by : com.renomad.minum.web.RequestTests.testRequest_ExpectComplaintAfterBegunReading_10(com.renomad.minum.web.RequestTests)
changed conditional boundary → KILLED

2.2
Location : contentType
Killed by : com.renomad.minum.web.RequestTests.testRequest_ExpectComplaintAfterBegunReading_10(com.renomad.minum.web.RequestTests)
negated conditional → KILLED

87

1.1
Location : contentType
Killed by : none
removed call to java/util/List::sort → SURVIVED
Covering tests

90

1.1
Location : contentType
Killed by : com.renomad.minum.web.RequestTests.testRequest_ExpectComplaintAfterBegunReading_10(com.renomad.minum.web.RequestTests)
negated conditional → KILLED

91

1.1
Location : contentType
Killed by : com.renomad.minum.web.RequestTests.testRequest_ExpectComplaintAfterBegunReading_10(com.renomad.minum.web.RequestTests)
replaced return value with "" for com/renomad/minum/web/Headers::contentType → KILLED

105

1.1
Location : contentLength
Killed by : com.renomad.minum.web.RequestTests.testRequest_ExpectComplaintAfterBegunReading_10(com.renomad.minum.web.RequestTests)
negated conditional → KILLED

2.2
Location : contentLength
Killed by : com.renomad.minum.web.RequestTests.testEndOfStreamWhileReadingStreamingMultipartPartition(com.renomad.minum.web.RequestTests)
changed conditional boundary → KILLED

106

1.1
Location : contentLength
Killed by : com.renomad.minum.web.HeadersTests
removed call to java/util/List::sort → KILLED

110

1.1
Location : contentLength
Killed by : com.renomad.minum.web.RequestTests.testRequest_ExpectComplaintAfterBegunReading_10(com.renomad.minum.web.RequestTests)
negated conditional → KILLED

115

1.1
Location : contentLength
Killed by : com.renomad.minum.web.RequestTests.testEndOfStreamWhileReadingStreamingMultipartPartition(com.renomad.minum.web.RequestTests)
replaced int return with 0 for com/renomad/minum/web/Headers::contentLength → KILLED

124

1.1
Location : hasKeepAlive
Killed by : com.renomad.minum.web.HeadersTests
negated conditional → KILLED

2.2
Location : hasKeepAlive
Killed by : com.renomad.minum.web.HeadersTests
replaced boolean return with true for com/renomad/minum/web/Headers::hasKeepAlive → KILLED

125

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

2.2
Location : lambda$hasKeepAlive$0
Killed by : com.renomad.minum.web.WebTests
replaced boolean return with true for com/renomad/minum/web/Headers::lambda$hasKeepAlive$0 → KILLED

3.3
Location : hasKeepAlive
Killed by : com.renomad.minum.web.HeadersTests
replaced boolean return with false for com/renomad/minum/web/Headers::hasKeepAlive → KILLED

4.4
Location : lambda$hasKeepAlive$0
Killed by : com.renomad.minum.web.HeadersTests
replaced boolean return with false for com/renomad/minum/web/Headers::lambda$hasKeepAlive$0 → KILLED

134

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

2.2
Location : hasConnectionClose
Killed by : com.renomad.minum.web.HeadersTests
negated conditional → KILLED

135

1.1
Location : hasConnectionClose
Killed by : com.renomad.minum.web.HeadersTests
replaced boolean return with false for com/renomad/minum/web/Headers::hasConnectionClose → KILLED

2.2
Location : hasConnectionClose
Killed by : com.renomad.minum.web.WebTests
replaced boolean return with true for com/renomad/minum/web/Headers::hasConnectionClose → KILLED

3.3
Location : lambda$hasConnectionClose$1
Killed by : com.renomad.minum.web.WebTests
replaced boolean return with true for com/renomad/minum/web/Headers::lambda$hasConnectionClose$1 → KILLED

4.4
Location : lambda$hasConnectionClose$1
Killed by : com.renomad.minum.web.HeadersTests
replaced boolean return with false for com/renomad/minum/web/Headers::lambda$hasConnectionClose$1 → KILLED

145

1.1
Location : getAllHeaders
Killed by : none
Changed increment from 1 to -1 → TIMED_OUT

146

1.1
Location : getAllHeaders
Killed by : com.renomad.minum.web.RequestTests.testOnlyHavingBoundaryValue(com.renomad.minum.web.RequestTests)
negated conditional → KILLED

2.2
Location : getAllHeaders
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
changed conditional boundary → KILLED

155

1.1
Location : getAllHeaders
Killed by : com.renomad.minum.web.RequestTests.testOnlyHavingBoundaryValue(com.renomad.minum.web.RequestTests)
negated conditional → KILLED

2.2
Location : getAllHeaders
Killed by : com.renomad.minum.web.HeadersTests
negated conditional → KILLED

157

1.1
Location : getAllHeaders
Killed by : com.renomad.minum.web.RequestTests.testOnlyHavingBoundaryValue(com.renomad.minum.web.RequestTests)
negated conditional → KILLED

158

1.1
Location : getAllHeaders
Killed by : com.renomad.minum.FunctionalTests
replaced return value with Collections.emptyList for com/renomad/minum/web/Headers::getAllHeaders → KILLED

163

1.1
Location : getAllHeaders
Killed by : com.renomad.minum.web.RequestTests.test_Request_getMultipartForm_EdgeCase_PlayingWithClose(com.renomad.minum.web.RequestTests)
replaced return value with Collections.emptyList for com/renomad/minum/web/Headers::getAllHeaders → KILLED

172

1.1
Location : valueByKey
Killed by : com.renomad.minum.web.RangeTests.test_NoRange(com.renomad.minum.web.RangeTests)
replaced return value with Collections.emptyList for com/renomad/minum/web/Headers::valueByKey → KILLED

177

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

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

178

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

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

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

180

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

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

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

185

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

190

1.1
Location : toString
Killed by : com.renomad.minum.web.RequestTests.test_Request_ToString(com.renomad.minum.web.RequestTests)
replaced return value with "" for com/renomad/minum/web/Headers::toString → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0