Body.java

1
package com.renomad.minum.web;
2
3
import com.renomad.minum.utils.StringUtils;
4
5
import java.util.*;
6
7
/**
8
 * This class represents the body of an HTML message.
9
 * See <a href="https://en.wikipedia.org/wiki/HTTP_message_body">Message Body on Wikipedia</a>
10
 *<br>
11
 * <pre>{@code
12
 * This could be a response from the web server:
13
 *
14
 * HTTP/1.1 200 OK
15
 * Date: Sun, 10 Oct 2010 23:26:07 GMT
16
 * Server: Apache/2.2.8 (Ubuntu) mod_ssl/2.2.8 OpenSSL/0.9.8g
17
 * Last-Modified: Sun, 26 Sep 2010 22:04:35 GMT
18
 * ETag: "45b6-834-49130cc1182c0"
19
 * Accept-Ranges: bytes
20
 * Content-Length: 12
21
 * Connection: close
22
 * Content-Type: text/html
23
 *
24
 * Hello world!
25
 * }</pre>
26
 * <p>
27
 *     The message body (or content) in this example is the text <pre>Hello world!</pre>.
28
 * </p>
29
 */
30
public final class Body {
31
32
    private static final byte[] EMPTY_BYTES = new byte[0];
33
    private final Map<String, byte[]> bodyMap;
34
    private final byte[] raw;
35
    private final List<Partition> partitions;
36
    private final BodyType bodyType;
37
38
    /**
39
     * An empty body instance, useful when you
40
     * need an instantiated body.
41
     */
42
    public static final Body EMPTY = new Body(Map.of(), EMPTY_BYTES, List.of(), BodyType.NONE);
43
44
    /**
45
     * Build a body for an HTTP message
46
     * @param bodyMap a map of key-value pairs, presumably extracted from form data.  Empty
47
     *                if our body isn't one of the form data protocols we understand.
48
     * @param raw the raw bytes of this body
49
     * @param partitions if the body is of type form/multipart, these will be the list of partitions
50
     */
51
    public Body(Map<String, byte[]> bodyMap, byte[] raw, List<Partition> partitions, BodyType bodyType) {
52
        this.bodyMap = new HashMap<>(bodyMap);
53
        this.raw = raw.clone();
54
        this.partitions = partitions;
55
        this.bodyType = bodyType;
56
    }
57
58
    /**
59
     * Return the value for a key, as a string. This method
60
     * presumes the data was sent URL-encoded.
61
     * <p>
62
     *     If there is no value found for the
63
     *     provided key, an empty string will be
64
     *     returned.
65
     * </p>
66
     * <p>
67
     *     Otherwise, the value found will be converted
68
     *     to a string, and trimmed.
69
     * </p>
70
     * <p>
71
     *     Note: if the request is a multipart/form-data, this
72
     *     method will throw a helpful exception to indicate that.
73
     * </p>
74
     * <p>
75
     *     Note: in the case of duplicate keys, last-in wins
76
     * </p>
77
     *
78
     */
79
    public String asString(String key) {
80 1 1. asString : negated conditional → KILLED
        if (this.equals(EMPTY)) {
81
            return "";
82
        }
83 1 1. asString : negated conditional → KILLED
        if (this.bodyType.equals(BodyType.MULTIPART)) {
84
            throw new WebServerException("Request body is in multipart format.  Use .getPartitionByName instead");
85
        }
86 1 1. asString : negated conditional → KILLED
        if (this.bodyType.equals(BodyType.UNRECOGNIZED)) {
87
            throw new WebServerException("Request body is not in a recognized key-value encoding.  Use .asString() to obtain the body data");
88
        }
89
        byte[] byteArray = bodyMap.get(key);
90 1 1. asString : negated conditional → KILLED
        if (byteArray == null) {
91
            return "";
92
        } else {
93 1 1. asString : replaced return value with "" for com/renomad/minum/web/Body::asString → KILLED
            return StringUtils.byteArrayToString(byteArray).trim();
94
        }
95
96
    }
97
98
    /**
99
     * Return the entire raw contents of the body of this
100
     * request, as a string. No processing involved other
101
     * than converting the bytes to a string.
102
     */
103
    public String asString() {
104 1 1. asString : negated conditional → TIMED_OUT
        if (this.equals(EMPTY)) {
105
            return "";
106
        }
107 1 1. asString : replaced return value with "" for com/renomad/minum/web/Body::asString → KILLED
        return StringUtils.byteArrayToString(raw).trim();
108
    }
109
110
    /**
111
     * Return the bytes of this request body by its key.  This method
112
     * presumes the data was sent URL-encoded.
113
     */
114
    public byte[] asBytes(String key) {
115 1 1. asBytes : negated conditional → KILLED
        if (this.equals(EMPTY)) {
116 1 1. asBytes : replaced return value with null for com/renomad/minum/web/Body::asBytes → KILLED
            return new byte[0];
117
        }
118 1 1. asBytes : negated conditional → KILLED
        if (this.bodyType.equals(BodyType.MULTIPART)) {
119
            throw new WebServerException("Request body is in multipart format.  Use .getPartitionByName instead");
120
        }
121 1 1. asBytes : negated conditional → KILLED
        if (this.bodyType.equals(BodyType.UNRECOGNIZED)) {
122
            throw new WebServerException("Request body is not in a recognized key-value encoding.  Use .asBytes() to obtain the body data");
123
        }
124 1 1. asBytes : replaced return value with null for com/renomad/minum/web/Body::asBytes → KILLED
        return bodyMap.get(key);
125
    }
126
127
    /**
128
     * Returns the raw bytes of this HTTP message's body. This method
129
     * presumes the data was sent URL-encoded.
130
     */
131
    public byte[] asBytes() {
132 1 1. asBytes : negated conditional → KILLED
        if (this.equals(EMPTY)) {
133 1 1. asBytes : replaced return value with null for com/renomad/minum/web/Body::asBytes → KILLED
            return new byte[0];
134
        }
135 1 1. asBytes : replaced return value with null for com/renomad/minum/web/Body::asBytes → KILLED
        return this.raw.clone();
136
    }
137
138
    /**
139
     * If the body is of type form/multipart, return the partitions
140
     * <p>
141
     *     For example:
142
     * </p>
143
     * <pre>
144
     * --i_am_a_boundary
145
     *  Content-Type: text/plain
146
     *  Content-Disposition: form-data; name="text1"
147
     *
148
     *  I am a value that is text
149
     *  --i_am_a_boundary
150
     *  Content-Type: application/octet-stream
151
     *  Content-Disposition: form-data; name="image_uploads"; filename="photo_preview.jpg"
152
     * </pre>
153
     */
154
    public List<Partition> getPartitionHeaders() {
155 1 1. getPartitionHeaders : negated conditional → KILLED
        if (this.equals(EMPTY)) {
156
            return List.of();
157
        }
158 1 1. getPartitionHeaders : negated conditional → KILLED
        if (this.bodyType.equals(BodyType.FORM_URL_ENCODED)) {
159
            throw new WebServerException("Request body encoded in form-urlencoded format. getPartitionHeaders is only used with multipart encoded data.");
160
        }
161 1 1. getPartitionHeaders : negated conditional → KILLED
        if (this.bodyType.equals(BodyType.UNRECOGNIZED)) {
162
            throw new WebServerException("Request body encoded is not encoded in a recognized format. getPartitionHeaders is only used with multipart encoded data.");
163
        }
164 1 1. getPartitionHeaders : replaced return value with Collections.emptyList for com/renomad/minum/web/Body::getPartitionHeaders → KILLED
        return new ArrayList<>(partitions);
165
    }
166
167
    /**
168
     * A helper method for getting the partitions with a particular name set in its
169
     * content-disposition.  This returns a list of partitions because there is nothing
170
     * preventing the browser doing this, and in fact it will typically send partitions
171
     * with the same name when sending multiple files from one input.  (HTML5 provides the
172
     * ability to select multiple files on the input with type=file)
173
     */
174
    public List<Partition> getPartitionByName(String name) {
175 1 1. getPartitionByName : negated conditional → KILLED
        if (this.equals(EMPTY)) {
176
            return List.of();
177
        }
178 1 1. getPartitionByName : negated conditional → KILLED
        if (this.bodyType.equals(BodyType.FORM_URL_ENCODED)) {
179
            throw new WebServerException("Request body encoded in form-urlencoded format. use .asString(key) or asBytes(key)");
180
        }
181 1 1. getPartitionByName : negated conditional → KILLED
        if (this.bodyType.equals(BodyType.UNRECOGNIZED)) {
182
            throw new WebServerException("Request body encoded is not encoded in a recognized format. use .asString() or asBytes()");
183
        }
184 3 1. getPartitionByName : replaced return value with Collections.emptyList for com/renomad/minum/web/Body::getPartitionByName → KILLED
2. lambda$getPartitionByName$0 : replaced boolean return with true for com/renomad/minum/web/Body::lambda$getPartitionByName$0 → KILLED
3. lambda$getPartitionByName$0 : replaced boolean return with false for com/renomad/minum/web/Body::lambda$getPartitionByName$0 → KILLED
        return getPartitionHeaders().stream().filter(x -> x.getContentDisposition().getName().equalsIgnoreCase(name)).toList();
185
    }
186
187
    /**
188
     * Returns the {@link BodyType}, which is necessary to distinguish
189
     * which methods to run for accessing data. For instance, if the body
190
     * is of type FORM_URL_ENCODED, you may use methods
191
     * like {@link #getKeys()}, {@link #asBytes(String)}, or {@link #asString(String)}
192
     * <br>
193
     * On the other hand, if the type is MULTIPART, you will need to use {@link #getPartitionHeaders()}
194
     * to get a list of the partitions.
195
     * <br>
196
     * If the body type is UNRECOGNIZED, you can use {@link #asBytes()} to get the body.
197
     * <br>
198
     * Don't forget, there is also an option to obtain the body's {@link java.io.InputStream} by
199
     * using {@link Request#getSocketWrapper()}, but that needs to be done before running {@link Request#getBody()}
200
     */
201
    public BodyType getBodyType() {
202 1 1. getBodyType : replaced return value with null for com/renomad/minum/web/Body::getBodyType → KILLED
        return bodyType;
203
    }
204
205
    /**
206
     * Get all the keys for the key-value pairs in the body
207
     */
208
    public Set<String> getKeys() {
209 1 1. getKeys : negated conditional → KILLED
        if (this.equals(EMPTY)) {
210
            return Set.of();
211
        }
212 1 1. getKeys : replaced return value with Collections.emptySet for com/renomad/minum/web/Body::getKeys → KILLED
        return bodyMap.keySet();
213
    }
214
215
    @Override
216
    public boolean equals(Object o) {
217 2 1. equals : negated conditional → KILLED
2. equals : replaced boolean return with false for com/renomad/minum/web/Body::equals → KILLED
        if (this == o) return true;
218 3 1. equals : replaced boolean return with true for com/renomad/minum/web/Body::equals → TIMED_OUT
2. equals : negated conditional → KILLED
3. equals : negated conditional → KILLED
        if (o == null || getClass() != o.getClass()) return false;
219
        Body body = (Body) o;
220 5 1. equals : negated conditional → KILLED
2. equals : replaced boolean return with true for com/renomad/minum/web/Body::equals → KILLED
3. equals : negated conditional → KILLED
4. equals : negated conditional → KILLED
5. equals : negated conditional → KILLED
        return Objects.equals(bodyMap, body.bodyMap) && Arrays.equals(raw, body.raw) && Objects.equals(partitions, body.partitions) && bodyType == body.bodyType;
221
    }
222
223
    @Override
224
    public int hashCode() {
225
        int result = Objects.hash(bodyMap, partitions, bodyType);
226 2 1. hashCode : Replaced integer multiplication with division → TIMED_OUT
2. hashCode : Replaced integer addition with subtraction → TIMED_OUT
        result = 31 * result + Arrays.hashCode(raw);
227 1 1. hashCode : replaced int return with 0 for com/renomad/minum/web/Body::hashCode → TIMED_OUT
        return result;
228
    }
229
230
    @Override
231
    public String toString() {
232 1 1. toString : replaced return value with "" for com/renomad/minum/web/Body::toString → KILLED
        return "Body{" +
233
                "bodyMap=" + bodyMap +
234
                ", bodyType=" + bodyType +
235
                '}';
236
    }
237
238
}

Mutations

80

1.1
Location : asString
Killed by : com.renomad.minum.web.BodyTests.testAsString_EdgeCase_BodyIsUnrecognized(com.renomad.minum.web.BodyTests)
negated conditional → KILLED

83

1.1
Location : asString
Killed by : com.renomad.minum.web.BodyTests.testGettingValue_EdgeCase_MissingKey(com.renomad.minum.web.BodyTests)
negated conditional → KILLED

86

1.1
Location : asString
Killed by : com.renomad.minum.web.BodyTests.testGettingValue_EdgeCase_MissingKey(com.renomad.minum.web.BodyTests)
negated conditional → KILLED

90

1.1
Location : asString
Killed by : com.renomad.minum.web.BodyTests.testGettingValue_EdgeCase_MissingKey(com.renomad.minum.web.BodyTests)
negated conditional → KILLED

93

1.1
Location : asString
Killed by : com.renomad.minum.web.BodyProcessorTests
replaced return value with "" for com/renomad/minum/web/Body::asString → KILLED

104

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

107

1.1
Location : asString
Killed by : com.renomad.minum.web.WebPerformanceTests.test2(com.renomad.minum.web.WebPerformanceTests)
replaced return value with "" for com/renomad/minum/web/Body::asString → KILLED

115

1.1
Location : asBytes
Killed by : com.renomad.minum.web.BodyTests.testAsBytes_EdgeCase_Empty(com.renomad.minum.web.BodyTests)
negated conditional → KILLED

116

1.1
Location : asBytes
Killed by : com.renomad.minum.web.BodyTests.testAsBytes_EdgeCase_Empty(com.renomad.minum.web.BodyTests)
replaced return value with null for com/renomad/minum/web/Body::asBytes → KILLED

118

1.1
Location : asBytes
Killed by : com.renomad.minum.web.BodyTests.testAsBytes_EdgeCase_BodyIsUnrecognized(com.renomad.minum.web.BodyTests)
negated conditional → KILLED

121

1.1
Location : asBytes
Killed by : com.renomad.minum.web.BodyTests.testAsBytes_EdgeCase_BodyIsUnrecognized(com.renomad.minum.web.BodyTests)
negated conditional → KILLED

124

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

132

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

133

1.1
Location : asBytes
Killed by : com.renomad.minum.web.BodyTests.testAsBytes_EdgeCase_Empty_2(com.renomad.minum.web.BodyTests)
replaced return value with null for com/renomad/minum/web/Body::asBytes → KILLED

135

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

155

1.1
Location : getPartitionHeaders
Killed by : com.renomad.minum.web.BodyTests.testGetPartitionHeaders_EdgeCase_BodyIsUnrecognized(com.renomad.minum.web.BodyTests)
negated conditional → KILLED

158

1.1
Location : getPartitionHeaders
Killed by : com.renomad.minum.web.BodyTests.testGetPartitionHeaders_EdgeCase_BodyIsUnrecognized(com.renomad.minum.web.BodyTests)
negated conditional → KILLED

161

1.1
Location : getPartitionHeaders
Killed by : com.renomad.minum.web.BodyTests.testGetPartitionHeaders_EdgeCase_BodyIsUnrecognized(com.renomad.minum.web.BodyTests)
negated conditional → KILLED

164

1.1
Location : getPartitionHeaders
Killed by : com.renomad.minum.web.BodyProcessorTests
replaced return value with Collections.emptyList for com/renomad/minum/web/Body::getPartitionHeaders → KILLED

175

1.1
Location : getPartitionByName
Killed by : com.renomad.minum.web.BodyTests.testGetPartitionByName_EdgeCase_Unrecognized(com.renomad.minum.web.BodyTests)
negated conditional → KILLED

178

1.1
Location : getPartitionByName
Killed by : com.renomad.minum.web.BodyTests.testGetPartitionByName_EdgeCase_Unrecognized(com.renomad.minum.web.BodyTests)
negated conditional → KILLED

181

1.1
Location : getPartitionByName
Killed by : com.renomad.minum.web.BodyTests.testGetPartitionByName_EdgeCase_Unrecognized(com.renomad.minum.web.BodyTests)
negated conditional → KILLED

184

1.1
Location : getPartitionByName
Killed by : com.renomad.minum.web.BodyProcessorTests
replaced return value with Collections.emptyList for com/renomad/minum/web/Body::getPartitionByName → KILLED

2.2
Location : lambda$getPartitionByName$0
Killed by : com.renomad.minum.web.BodyProcessorTests
replaced boolean return with true for com/renomad/minum/web/Body::lambda$getPartitionByName$0 → KILLED

3.3
Location : lambda$getPartitionByName$0
Killed by : com.renomad.minum.web.BodyProcessorTests
replaced boolean return with false for com/renomad/minum/web/Body::lambda$getPartitionByName$0 → KILLED

202

1.1
Location : getBodyType
Killed by : com.renomad.minum.web.BodyProcessorTests
replaced return value with null for com/renomad/minum/web/Body::getBodyType → KILLED

209

1.1
Location : getKeys
Killed by : com.renomad.minum.web.BodyProcessorTests
negated conditional → KILLED

212

1.1
Location : getKeys
Killed by : com.renomad.minum.web.BodyProcessorTests
replaced return value with Collections.emptySet for com/renomad/minum/web/Body::getKeys → KILLED

217

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

2.2
Location : equals
Killed by : com.renomad.minum.web.BodyTests.testAsBytes_EdgeCase_Empty(com.renomad.minum.web.BodyTests)
replaced boolean return with false for com/renomad/minum/web/Body::equals → KILLED

218

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

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

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

220

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

2.2
Location : equals
Killed by : com.renomad.minum.web.BodyTests.testGetPartitionByName_EdgeCase_Unrecognized(com.renomad.minum.web.BodyTests)
replaced boolean return with true for com/renomad/minum/web/Body::equals → KILLED

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

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

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

226

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

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

227

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

232

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

Active mutators

Tests examined


Report generated by PIT 1.17.0