RequestLine.java

1
package com.renomad.minum.web;
2
3
import com.renomad.minum.logging.ILogger;
4
import com.renomad.minum.security.ForbiddenUseException;
5
import com.renomad.minum.utils.StringUtils;
6
7
import java.util.*;
8
import java.util.regex.Matcher;
9
import java.util.regex.Pattern;
10
11
import static com.renomad.minum.utils.Invariants.mustNotBeNull;
12
13
/**
14
 * This class holds data and methods for dealing with the
15
 * "start line" in an HTTP request.  For example,
16
 * GET /foo HTTP/1.1
17
 */
18
public final class RequestLine {
19
20
    private final Method method;
21
    private final PathDetails pathDetails;
22
    private final HttpVersion version;
23
    private final String rawValue;
24
    private final ILogger logger;
25
    static final int MAX_QUERY_STRING_KEYS_COUNT = 50;
26
27
    /**
28
     * @param method GET, POST, etc.
29
     * @param pathDetails See {@link PathDetails}
30
     * @param version the version of HTTP (1.0 or 1.1) we're receiving
31
     * @param rawValue the entire raw string of the start line
32
     */
33
    public RequestLine(
34
            Method method,
35
            PathDetails pathDetails,
36
            HttpVersion version,
37
            String rawValue,
38
            ILogger logger
39
    ) {
40
        this.method = method;
41
        this.pathDetails = pathDetails;
42
        this.version = version;
43
        this.rawValue = rawValue;
44
        this.logger = logger;
45
    }
46
47
    /**
48
     * This is our regex for looking at a client's request
49
     * and determining what to send them.  For example,
50
     * if they send GET /sample.html HTTP/1.1, we send them sample.html
51
     * <p>
52
     * On the other hand if it's not a well-formed request, or
53
     * if we don't have that file, we reply with an error page
54
     * </p>
55
     */
56
    static final String REQUEST_LINE_PATTERN = "^([A-Z]{3,8})" + // an HTTP method, like GET, HEAD, POST, or OPTIONS
57
            " /?(.*)" + // the request target - may or may not start with a slash.
58
            " HTTP/(1.1|1.0)$"; // the HTTP version, defining structure of the remaining message
59
60
    static final Pattern startLineRegex = Pattern.compile(REQUEST_LINE_PATTERN);
61
62
    public static final RequestLine EMPTY = new RequestLine(Method.NONE, PathDetails.empty, HttpVersion.NONE, "", null);
63
64
    /**
65
     * Returns a map of the key-value pairs in the URL,
66
     * for example in {@code http://foo.com?name=alice} you
67
     * have a key of name and a value of alice.
68
     */
69
    public Map<String, String> queryString() {
70 2 1. queryString : negated conditional → KILLED
2. queryString : negated conditional → KILLED
        if (pathDetails == null || pathDetails.getQueryString().isEmpty()) {
71 1 1. queryString : replaced return value with Collections.emptyMap for com/renomad/minum/web/RequestLine::queryString → SURVIVED
            return new HashMap<>();
72
        } else {
73 1 1. queryString : replaced return value with Collections.emptyMap for com/renomad/minum/web/RequestLine::queryString → KILLED
            return new HashMap<>(pathDetails.getQueryString());
74
        }
75
76
    }
77
78
    /**
79
     * These are the HTTP methods we handle.
80
     * @see #REQUEST_LINE_PATTERN
81
     */
82
    public enum Method {
83
        GET,
84
        POST,
85
        PUT,
86
        DELETE,
87
        TRACE,
88
        PATCH,
89
        OPTIONS,
90
        HEAD,
91
92
        /**
93
         * Represents the null value of Method
94
         */
95
        NONE
96
    }
97
98
    /**
99
     * Given the string value of a Request Line (like GET /hello HTTP/1.1)
100
     * validate and extract the values for our use.
101
     */
102
    public RequestLine extractRequestLine(String value) {
103
        mustNotBeNull(value);
104
        Matcher m = RequestLine.startLineRegex.matcher(value);
105
        // run the regex
106
        var doesMatch = m.matches();
107 1 1. extractRequestLine : negated conditional → KILLED
        if (!doesMatch) {
108 1 1. extractRequestLine : replaced return value with null for com/renomad/minum/web/RequestLine::extractRequestLine → KILLED
            return RequestLine.EMPTY;
109
        }
110
        Method myMethod = extractMethod(m.group(1));
111
        PathDetails pd = extractPathDetails(m.group(2));
112
        HttpVersion httpVersion = getHttpVersion(m.group(3));
113
114 1 1. extractRequestLine : replaced return value with null for com/renomad/minum/web/RequestLine::extractRequestLine → KILLED
        return new RequestLine(myMethod, pd, httpVersion, value, logger);
115
    }
116
117
    private Method extractMethod(String methodString) {
118
        try {
119 1 1. extractMethod : replaced return value with null for com/renomad/minum/web/RequestLine::extractMethod → KILLED
            return Method.valueOf(methodString.toUpperCase(Locale.ROOT));
120
        } catch (Exception ex) {
121
            logger.logDebug(() -> "Unable to convert method to enum: " + methodString);
122 1 1. extractMethod : replaced return value with null for com/renomad/minum/web/RequestLine::extractMethod → TIMED_OUT
            return Method.NONE;
123
        }
124
    }
125
126
    private PathDetails extractPathDetails(String path) {
127
        PathDetails pd;
128
        int locationOfQueryBegin = path.indexOf("?");
129 2 1. extractPathDetails : negated conditional → KILLED
2. extractPathDetails : changed conditional boundary → KILLED
        if (locationOfQueryBegin > 0) {
130
            // in this case, we found a question mark, suggesting that a query string exists
131 1 1. extractPathDetails : Replaced integer addition with subtraction → KILLED
            String rawQueryString = path.substring(locationOfQueryBegin + 1);
132
            String isolatedPath = path.substring(0, locationOfQueryBegin);
133
            Map<String, String> queryString = extractMapFromQueryString(rawQueryString);
134
            pd = new PathDetails(isolatedPath, rawQueryString, queryString);
135
        } else {
136
            // in this case, no question mark was found, thus no query string
137
            pd = new PathDetails(path, null, null);
138
        }
139 1 1. extractPathDetails : replaced return value with null for com/renomad/minum/web/RequestLine::extractPathDetails → KILLED
        return pd;
140
    }
141
142
143
    /**
144
     * Given a string containing the combined key-values in
145
     * a query string (e.g. foo=bar&name=alice), split that
146
     * into a map of the key to value (e.g. foo to bar, and name to alice)
147
     */
148
    Map<String, String> extractMapFromQueryString(String rawQueryString) {
149
        Map<String, String> queryStrings = new HashMap<>();
150
        StringTokenizer tokenizer = new StringTokenizer(rawQueryString, "&");
151
        // we'll only take less than MAX_QUERY_STRING_KEYS_COUNT
152 2 1. extractMapFromQueryString : Changed increment from 1 to -1 → KILLED
2. extractMapFromQueryString : negated conditional → KILLED
        for (int i = 0; tokenizer.hasMoreTokens(); i++) {
153 2 1. extractMapFromQueryString : changed conditional boundary → SURVIVED
2. extractMapFromQueryString : negated conditional → TIMED_OUT
            if (i >= MAX_QUERY_STRING_KEYS_COUNT) throw new ForbiddenUseException("User tried providing too many query string keys.  max: " + MAX_QUERY_STRING_KEYS_COUNT);
154
            // this should give us a key and value joined with an equal sign, e.g. foo=bar
155
            String currentKeyValue = tokenizer.nextToken();
156
            int equalSignLocation = currentKeyValue.indexOf("=");
157 2 1. extractMapFromQueryString : changed conditional boundary → TIMED_OUT
2. extractMapFromQueryString : negated conditional → KILLED
            if (equalSignLocation <= 0) return Map.of();
158
            String key = currentKeyValue.substring(0, equalSignLocation);
159 1 1. extractMapFromQueryString : Replaced integer addition with subtraction → TIMED_OUT
            String value = StringUtils.decode(currentKeyValue.substring(equalSignLocation + 1));
160
            queryStrings.put(key, value);
161
        }
162 1 1. extractMapFromQueryString : replaced return value with Collections.emptyMap for com/renomad/minum/web/RequestLine::extractMapFromQueryString → TIMED_OUT
        return queryStrings;
163
    }
164
165
    /**
166
     * Extract the HTTP version from the start line
167
     */
168
    private HttpVersion getHttpVersion(String version) {
169 1 1. getHttpVersion : negated conditional → KILLED
        if (version.equals("1.1")) {
170 1 1. getHttpVersion : replaced return value with null for com/renomad/minum/web/RequestLine::getHttpVersion → TIMED_OUT
            return HttpVersion.ONE_DOT_ONE;
171
        } else {
172 1 1. getHttpVersion : replaced return value with null for com/renomad/minum/web/RequestLine::getHttpVersion → KILLED
            return HttpVersion.ONE_DOT_ZERO;
173
        }
174
    }
175
176
    /**
177
     * Return the method of this request-line.  For example, GET, PUT, POST...
178
     */
179
    public Method getMethod() {
180 1 1. getMethod : replaced return value with null for com/renomad/minum/web/RequestLine::getMethod → KILLED
        return method;
181
    }
182
183
    /**
184
     * This returns an object which contains essential information about the path
185
     * in the request line.  For example, if the request line is "GET /sample?foo=bar HTTP/1.1",
186
     * this would hold data for the path ("sample") and the query string ("foo=bar")
187
     */
188
    public PathDetails getPathDetails() {
189 1 1. getPathDetails : replaced return value with null for com/renomad/minum/web/RequestLine::getPathDetails → KILLED
        return pathDetails;
190
    }
191
192
    /**
193
     * Gets the HTTP version, either 1.0 or 1.1
194
     */
195
    public HttpVersion getVersion() {
196 1 1. getVersion : replaced return value with null for com/renomad/minum/web/RequestLine::getVersion → KILLED
        return this.version;
197
    }
198
199
    /**
200
     * Get the string value of this request line, such as "GET /sample.html HTTP/1.1"
201
     */
202
    public String getRawValue() {
203 1 1. getRawValue : replaced return value with "" for com/renomad/minum/web/RequestLine::getRawValue → KILLED
        return rawValue;
204
    }
205
206
    @Override
207
    public boolean equals(Object o) {
208 2 1. equals : negated conditional → TIMED_OUT
2. equals : replaced boolean return with false for com/renomad/minum/web/RequestLine::equals → KILLED
        if (this == o) return true;
209 3 1. equals : negated conditional → TIMED_OUT
2. equals : replaced boolean return with true for com/renomad/minum/web/RequestLine::equals → TIMED_OUT
3. equals : negated conditional → TIMED_OUT
        if (o == null || getClass() != o.getClass()) return false;
210
        RequestLine that = (RequestLine) o;
211 6 1. equals : negated conditional → TIMED_OUT
2. equals : negated conditional → TIMED_OUT
3. equals : negated conditional → TIMED_OUT
4. equals : negated conditional → TIMED_OUT
5. equals : replaced boolean return with true for com/renomad/minum/web/RequestLine::equals → TIMED_OUT
6. equals : negated conditional → TIMED_OUT
        return method == that.method && Objects.equals(pathDetails, that.pathDetails) && version == that.version && Objects.equals(rawValue, that.rawValue) && Objects.equals(logger, that.logger);
212
    }
213
214
    @Override
215
    public int hashCode() {
216 1 1. hashCode : replaced int return with 0 for com/renomad/minum/web/RequestLine::hashCode → TIMED_OUT
        return Objects.hash(method, pathDetails, version, rawValue, logger);
217
    }
218
219
    @Override
220
    public String toString() {
221 1 1. toString : replaced return value with "" for com/renomad/minum/web/RequestLine::toString → KILLED
        return "RequestLine{" +
222
                "method=" + method +
223
                ", pathDetails=" + pathDetails +
224
                ", version=" + version +
225
                ", rawValue='" + rawValue + '\'' +
226
                ", logger=" + logger +
227
                '}';
228
    }
229
}

Mutations

70

1.1
Location : queryString
Killed by : com.renomad.minum.web.EndpointTests.test_Endpoint_HappyPath(com.renomad.minum.web.EndpointTests)
negated conditional → KILLED

2.2
Location : queryString
Killed by : com.renomad.minum.web.EndpointTests.test_Endpoint_HappyPath(com.renomad.minum.web.EndpointTests)
negated conditional → KILLED

71

1.1
Location : queryString
Killed by : none
replaced return value with Collections.emptyMap for com/renomad/minum/web/RequestLine::queryString → SURVIVED
Covering tests

73

1.1
Location : queryString
Killed by : com.renomad.minum.web.EndpointTests.test_Endpoint_HappyPath(com.renomad.minum.web.EndpointTests)
replaced return value with Collections.emptyMap for com/renomad/minum/web/RequestLine::queryString → KILLED

107

1.1
Location : extractRequestLine
Killed by : com.renomad.minum.web.WebTests
negated conditional → KILLED

108

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

114

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

119

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

122

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

129

1.1
Location : extractPathDetails
Killed by : com.renomad.minum.web.WebTests
negated conditional → KILLED

2.2
Location : extractPathDetails
Killed by : com.renomad.minum.FunctionalTests
changed conditional boundary → KILLED

131

1.1
Location : extractPathDetails
Killed by : com.renomad.minum.web.WebTests
Replaced integer addition with subtraction → KILLED

139

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

152

1.1
Location : extractMapFromQueryString
Killed by : com.renomad.minum.web.WebTests
Changed increment from 1 to -1 → KILLED

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

153

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

2.2
Location : extractMapFromQueryString
Killed by : none
changed conditional boundary → SURVIVED
Covering tests

157

1.1
Location : extractMapFromQueryString
Killed by : com.renomad.minum.web.WebTests
negated conditional → KILLED

2.2
Location : extractMapFromQueryString
Killed by : none
changed conditional boundary → TIMED_OUT

159

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

162

1.1
Location : extractMapFromQueryString
Killed by : none
replaced return value with Collections.emptyMap for com/renomad/minum/web/RequestLine::extractMapFromQueryString → TIMED_OUT

169

1.1
Location : getHttpVersion
Killed by : com.renomad.minum.web.WebTests
negated conditional → KILLED

170

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

172

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

180

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

189

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

196

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

203

1.1
Location : getRawValue
Killed by : com.renomad.minum.web.RequestLineTests.test_GetRawValue(com.renomad.minum.web.RequestLineTests)
replaced return value with "" for com/renomad/minum/web/RequestLine::getRawValue → KILLED

208

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

2.2
Location : equals
Killed by : com.renomad.minum.web.RequestTests.testSimplerRequest(com.renomad.minum.web.RequestTests)
replaced boolean return with false for com/renomad/minum/web/RequestLine::equals → KILLED

209

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

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

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

211

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

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

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

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

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

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

216

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

221

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/RequestLine::toString → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0