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
     *
75
     */
76
    public String asString(String key) {
77 1 1. asString : negated conditional → KILLED
        if (this.equals(EMPTY)) {
78
            return "";
79
        }
80 1 1. asString : negated conditional → KILLED
        if (this.bodyType.equals(BodyType.MULTIPART)) {
81
            throw new WebServerException("Request body is in multipart format.  Use .getPartitionByName instead");
82
        }
83 1 1. asString : negated conditional → KILLED
        if (this.bodyType.equals(BodyType.UNRECOGNIZED)) {
84
            throw new WebServerException("Request body is not in a recognized key-value encoding.  Use .asString() to obtain the body data");
85
        }
86
        byte[] byteArray = bodyMap.get(key);
87 1 1. asString : negated conditional → KILLED
        if (byteArray == null) {
88
            return "";
89
        } else {
90 1 1. asString : replaced return value with "" for com/renomad/minum/web/Body::asString → KILLED
            return StringUtils.byteArrayToString(byteArray).trim();
91
        }
92
93
    }
94
95
    /**
96
     * Return the entire raw contents of the body of this
97
     * request, as a string. No processing involved other
98
     * than converting the bytes to a string.
99
     */
100
    public String asString() {
101 1 1. asString : negated conditional → KILLED
        if (this.equals(EMPTY)) {
102
            return "";
103
        }
104 1 1. asString : replaced return value with "" for com/renomad/minum/web/Body::asString → KILLED
        return StringUtils.byteArrayToString(raw).trim();
105
    }
106
107
    /**
108
     * Return the bytes of this request body by its key.  This method
109
     * presumes the data was sent URL-encoded.
110
     */
111
    public byte[] asBytes(String key) {
112 1 1. asBytes : negated conditional → KILLED
        if (this.equals(EMPTY)) {
113 1 1. asBytes : replaced return value with null for com/renomad/minum/web/Body::asBytes → KILLED
            return new byte[0];
114
        }
115 1 1. asBytes : negated conditional → KILLED
        if (this.bodyType.equals(BodyType.MULTIPART)) {
116
            throw new WebServerException("Request body is in multipart format.  Use .getPartitionByName instead");
117
        }
118 1 1. asBytes : negated conditional → KILLED
        if (this.bodyType.equals(BodyType.UNRECOGNIZED)) {
119
            throw new WebServerException("Request body is not in a recognized key-value encoding.  Use .asBytes() to obtain the body data");
120
        }
121 1 1. asBytes : replaced return value with null for com/renomad/minum/web/Body::asBytes → KILLED
        return bodyMap.get(key);
122
    }
123
124
    /**
125
     * Returns the raw bytes of this HTTP message's body. This method
126
     * presumes the data was sent URL-encoded.
127
     */
128
    public byte[] asBytes() {
129 1 1. asBytes : negated conditional → KILLED
        if (this.equals(EMPTY)) {
130 1 1. asBytes : replaced return value with null for com/renomad/minum/web/Body::asBytes → KILLED
            return new byte[0];
131
        }
132 1 1. asBytes : replaced return value with null for com/renomad/minum/web/Body::asBytes → KILLED
        return this.raw.clone();
133
    }
134
135
    /**
136
     * If the body is of type form/multipart, return the partitions
137
     * <p>
138
     *     For example:
139
     * </p>
140
     * <pre>
141
     * --i_am_a_boundary
142
     *  Content-Type: text/plain
143
     *  Content-Disposition: form-data; name="text1"
144
     *
145
     *  I am a value that is text
146
     *  --i_am_a_boundary
147
     *  Content-Type: application/octet-stream
148
     *  Content-Disposition: form-data; name="image_uploads"; filename="photo_preview.jpg"
149
     * </pre>
150
     */
151
    public List<Partition> getPartitionHeaders() {
152 1 1. getPartitionHeaders : negated conditional → KILLED
        if (this.equals(EMPTY)) {
153
            return List.of();
154
        }
155 1 1. getPartitionHeaders : negated conditional → KILLED
        if (this.bodyType.equals(BodyType.FORM_URL_ENCODED)) {
156
            throw new WebServerException("Request body encoded in form-urlencoded format. getPartitionHeaders is only used with multipart encoded data.");
157
        }
158 1 1. getPartitionHeaders : negated conditional → KILLED
        if (this.bodyType.equals(BodyType.UNRECOGNIZED)) {
159
            throw new WebServerException("Request body encoded is not encoded in a recognized format. getPartitionHeaders is only used with multipart encoded data.");
160
        }
161 1 1. getPartitionHeaders : replaced return value with Collections.emptyList for com/renomad/minum/web/Body::getPartitionHeaders → KILLED
        return new ArrayList<>(partitions);
162
    }
163
164
    /**
165
     * A helper method for getting the partitions with a particular name set in its
166
     * content-disposition.  This returns a list of partitions because there is nothing
167
     * preventing the browser doing this, and in fact it will typically send partitions
168
     * with the same name when sending multiple files from one input.  (HTML5 provides the
169
     * ability to select multiple files on the input with type=file)
170
     */
171
    public List<Partition> getPartitionByName(String name) {
172 1 1. getPartitionByName : negated conditional → KILLED
        if (this.equals(EMPTY)) {
173
            return List.of();
174
        }
175 1 1. getPartitionByName : negated conditional → KILLED
        if (this.bodyType.equals(BodyType.FORM_URL_ENCODED)) {
176
            throw new WebServerException("Request body encoded in form-urlencoded format. use .asString(key) or asBytes(key)");
177
        }
178 1 1. getPartitionByName : negated conditional → KILLED
        if (this.bodyType.equals(BodyType.UNRECOGNIZED)) {
179
            throw new WebServerException("Request body encoded is not encoded in a recognized format. use .asString() or asBytes()");
180
        }
181 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();
182
    }
183
184
    /**
185
     * Returns the {@link BodyType}, which is necessary to distinguish
186
     * which methods to run for accessing data. For instance, if the body
187
     * is of type FORM_URL_ENCODED, you may use methods
188
     * like {@link #getKeys()}, {@link #asBytes(String)}, or {@link #asString(String)}
189
     * <br>
190
     * On the other hand, if the type is MULTIPART, you will need to use {@link #getPartitionHeaders()}
191
     * to get a list of the partitions.
192
     * <br>
193
     * If the body type is UNRECOGNIZED, you can use {@link #asBytes()} to get the body.
194
     * <br>
195
     * Don't forget, there is also an option to obtain the body's {@link java.io.InputStream} by
196
     * using {@link Request#getSocketWrapper()}, but that needs to be done before running {@link Request#getBody()}
197
     */
198
    public BodyType getBodyType() {
199 1 1. getBodyType : replaced return value with null for com/renomad/minum/web/Body::getBodyType → KILLED
        return bodyType;
200
    }
201
202
    /**
203
     * Get all the keys for the key-value pairs in the body
204
     */
205
    public Set<String> getKeys() {
206 1 1. getKeys : negated conditional → KILLED
        if (this.equals(EMPTY)) {
207
            return Set.of();
208
        }
209 1 1. getKeys : replaced return value with Collections.emptySet for com/renomad/minum/web/Body::getKeys → KILLED
        return bodyMap.keySet();
210
    }
211
212
    @Override
213
    public boolean equals(Object o) {
214 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;
215 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;
216
        Body body = (Body) o;
217 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;
218
    }
219
220
    @Override
221
    public int hashCode() {
222
        int result = Objects.hash(bodyMap, partitions, bodyType);
223 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);
224 1 1. hashCode : replaced int return with 0 for com/renomad/minum/web/Body::hashCode → TIMED_OUT
        return result;
225
    }
226
227
    @Override
228
    public String toString() {
229 1 1. toString : replaced return value with "" for com/renomad/minum/web/Body::toString → KILLED
        return "Body{" +
230
                "bodyMap=" + bodyMap +
231
                ", bodyType=" + bodyType +
232
                '}';
233
    }
234
235
}

Mutations

77

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

80

1.1
Location : asString
Killed by : com.renomad.minum.web.BodyTests.testGettingValue_EdgeCase_MissingKey(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

87

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.BodyProcessorTests
replaced return value with "" for com/renomad/minum/web/Body::asString → KILLED

101

1.1
Location : asString
Killed by : com.renomad.minum.FunctionalTests
negated conditional → KILLED

104

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

112

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

113

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

115

1.1
Location : asBytes
Killed by : com.renomad.minum.web.BodyTests.testAsBytes_EdgeCase_BodyIsMultipart(com.renomad.minum.web.BodyTests)
negated conditional → 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.FunctionalTests
replaced return value with null for com/renomad/minum/web/Body::asBytes → KILLED

129

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

130

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

132

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

152

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

155

1.1
Location : getPartitionHeaders
Killed by : com.renomad.minum.web.BodyTests.testGetPartitionHeaders_EdgeCase_BodyIsUrlEncoded(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.BodyProcessorTests
replaced return value with Collections.emptyList for com/renomad/minum/web/Body::getPartitionHeaders → KILLED

172

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

175

1.1
Location : getPartitionByName
Killed by : com.renomad.minum.web.BodyTests.testGetPartitionByName_EdgeCase_UrlEncoded(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.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

199

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

206

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

209

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

214

1.1
Location : equals
Killed by : com.renomad.minum.web.BodyTests.testGetPartitionByName_EdgeCase_UrlEncoded(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

215

1.1
Location : equals
Killed by : com.renomad.minum.web.RequestTests.test_Request_ImproperlyFormedMultipart(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_ImproperlyFormedMultipart(com.renomad.minum.web.RequestTests)
negated conditional → KILLED

217

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

2.2
Location : equals
Killed by : com.renomad.minum.web.BodyTests.testGetPartitionHeaders_EdgeCase_BodyIsUrlEncoded(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_ImproperlyFormedMultipart(com.renomad.minum.web.RequestTests)
negated conditional → KILLED

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

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

223

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

224

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

229

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