Skip to content

Commit 30c0168

Browse files
committed
Add support for multiple query parameter values
This changes the API to `List<String>` for each parameter value. Even though most cases don't require this, it's important we provide functionality for it. In the future maybe we can make the API a bit better around these use cases.
1 parent 7802faf commit 30c0168

File tree

10 files changed

+122
-79
lines changed

10 files changed

+122
-79
lines changed

client/src/main/java/org/threadly/litesockets/client/http/HTTPClient.java

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -171,12 +171,18 @@ public void closeAllClients() {
171171
* Sets the default timeout in milliseconds to wait for HTTPRequest responses from the server.
172172
*
173173
* @param timeout time in milliseconds to wait for HTTPRequests to finish.
174+
* @param unit The unit the {@code timeout} value is represented in
174175
*/
175176
public void setTimeout(long timeout, TimeUnit unit) {
176177
this.defaultTimeoutMS = Math.min(Math.max(unit.toMillis(timeout),HTTPRequest.MIN_TIMEOUT_MS),
177178
HTTPRequest.MAX_TIMEOUT_MS);
178179
}
179180

181+
/**
182+
* Checks the configured maximum connection idle time.
183+
*
184+
* @return The number of milliseconds until an idle connection is closed
185+
*/
180186
public long getMaxIdleTimeout() {
181187
return this.maxIdleTime;
182188
}
@@ -185,10 +191,11 @@ public long getMaxIdleTimeout() {
185191
* Sets the max amount of time we will hold onto idle connections. A 0 means we close connections when done, less
186192
* than zero means we will never expire connections.
187193
*
188-
* @param it the time in milliseconds to wait before timing out a connection.
194+
* @param idleTimeout the time in milliseconds to wait before timing out a connection.
195+
* @param unit The unit the {@code idleTimeout} value is represented in
189196
*/
190-
public void setMaxIdleTimeout(long it, TimeUnit unit) {
191-
this.maxIdleTime = unit.toMillis(it);
197+
public void setMaxIdleTimeout(long idleTimeout, TimeUnit unit) {
198+
this.maxIdleTime = unit.toMillis(idleTimeout);
192199
if(this.maxIdleTime > 0) {
193200
this.checkIdle = new Runnable() {
194201
@Override
@@ -207,7 +214,7 @@ public void run() {
207214
* Sends a blocking HTTP request.
208215
*
209216
* @param url the url to send the request too.
210-
* @return an {@link HTTPResponseData} object containing the headers and content of the response.
217+
* @return A {@link HTTPResponseData} object containing the headers and content of the response.
211218
* @throws HTTPParsingException is thrown if the server sends back protocol or a response that is larger then allowed.
212219
*/
213220
public HTTPResponseData request(final URL url) throws HTTPParsingException {
@@ -220,7 +227,7 @@ public HTTPResponseData request(final URL url) throws HTTPParsingException {
220227
* @param url the url to send the request too.
221228
* @param rm the {@link HTTPRequestMethod} to use on the request.
222229
* @param bb the data to put in the body for this request.
223-
* @return an {@link HTTPResponseData} object containing the headers and content of the response.
230+
* @return A {@link HTTPResponseData} object containing the headers and content of the response.
224231
* @throws HTTPParsingException is thrown if the server sends back protocol or a response that is larger then allowed.
225232
*/
226233
public HTTPResponseData request(final URL url, final HTTPRequestMethod rm, final ByteBuffer bb) throws HTTPParsingException {
@@ -242,12 +249,8 @@ public HTTPResponseData request(final URL url, final HTTPRequestMethod rm, final
242249
/**
243250
* Sends a blocking HTTP request.
244251
*
245-
* @param ha the {@link HTTPAddress} to connect to, any hostname in the actual HTTPRequest will just be sent in the protocol.
246252
* @param request the {@link HTTPRequest} to send the server once connected.
247-
* @param body the body to send with this request. You must have set the {@link HTTPRequest} correctly for this body.
248-
* @param unit the time unit of the timeout argument
249-
* @param timeout the maximum time to wait
250-
* @return an {@link HTTPResponseData} object containing the headers and content of the response.
253+
* @return A {@link HTTPResponseData} object containing the headers and content of the response.
251254
* @throws HTTPParsingException is thrown if the server sends back protocol or a response that is larger then allowed.
252255
*/
253256
public HTTPResponseData request(final ClientHTTPRequest request) throws HTTPParsingException {
@@ -272,7 +275,7 @@ public HTTPResponseData request(final ClientHTTPRequest request) throws HTTPPars
272275
* Sends an asynchronous HTTP request.
273276
*
274277
* @param url the {@link URL} to send the request too.
275-
* @return an {@link ListenableFuture} containing a {@link HTTPResponseData} object that will be completed when the request is finished,
278+
* @return A {@link ListenableFuture} containing a {@link HTTPResponseData} object that will be completed when the request is finished,
276279
* successfully or with errors.
277280
*/
278281
public ListenableFuture<HTTPResponseData> requestAsync(final URL url) {
@@ -285,7 +288,7 @@ public ListenableFuture<HTTPResponseData> requestAsync(final URL url) {
285288
* @param url the {@link URL} to send the request too.
286289
* @param rm the {@link HTTPRequestMethod} to use on the request.
287290
* @param bb the data to put in the body for this request.
288-
* @return an {@link ListenableFuture} containing a {@link HTTPResponseData} object that will be completed when the request is finished,
291+
* @return A {@link ListenableFuture} containing a {@link HTTPResponseData} object that will be completed when the request is finished,
289292
* successfully or with errors.
290293
*/
291294
public ListenableFuture<HTTPResponseData> requestAsync(final URL url, final HTTPRequestMethod rm,
@@ -302,11 +305,7 @@ public ListenableFuture<HTTPResponseData> requestAsync(final URL url, final HTTP
302305
/**
303306
* Sends an asynchronous HTTP request.
304307
*
305-
* @param ha the {@link HTTPAddress} to connect to, any hostname in the actual HTTPRequest will just be sent in the protocol.
306308
* @param request the {@link HTTPRequest} to send the server once connected.
307-
* @param body the body to send with this request. You must have set the {@link HTTPRequest} correctly for this body.
308-
* @param unit the time unit of the timeout argument
309-
* @param timeout the maximum time to wait
310309
* @return an {@link ListenableFuture} containing a {@link HTTPResponseData} object that will be completed when the request is finished,
311310
* successfully or with errors.
312311
*/
@@ -509,7 +508,8 @@ public void onRead(Client client) {
509508
}
510509

511510
/**
512-
*
511+
* Wrapper for the request that will handle the incoming response data (as well as notifying
512+
* back to the client once the response is complete).
513513
*/
514514
private class HTTPRequestWrapper implements HTTPResponseCallback {
515515
private final SettableListenableFuture<HTTPResponseData> slf = new SettableListenableFuture<>(false);

client/src/main/java/org/threadly/litesockets/client/http/HTTPStreamClient.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,17 @@ public class HTTPStreamClient implements StreamingClient {
6262
* Creates an HTTPStreaming client from an already existing TCPClient.
6363
*
6464
* @param client the {@link TCPClient} to use for this connection.
65-
* @param headerSent true if the http headers have already been sent, false if they still need to be sent.
6665
*/
6766
public HTTPStreamClient(TCPClient client) {
6867
this(client, client.getRemoteSocketAddress().getHostName());
6968
}
70-
69+
70+
/**
71+
* Creates an HTTPStreaming client from an already existing TCPClient.
72+
*
73+
* @param client the {@link TCPClient} to use for this connection.
74+
* @param host the hostname or ip address to connect to.
75+
*/
7176
public HTTPStreamClient(TCPClient client, String host) {
7277
this.client = client;
7378
this.host = host;

client/src/main/java/org/threadly/litesockets/client/ws/WebSocketClient.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ public class WebSocketClient implements StreamingClient {
7171
* This takes over an existing TCPClient to do websocket communications.
7272
*
7373
* @param client the TCPClient to use for this connection.
74-
* @param alreadyUpgraded true if the connection has already upgraded to do websockets false if the http upgrade is still required.
7574
*/
7675
public WebSocketClient(final TCPClient client) {
7776
if(client.isClosed()) {
@@ -196,7 +195,7 @@ public boolean getPingAutoReply() {
196195
* Sets the default {@link WebSocketOpCode} to use when calling {@link #write(ByteBuffer)}.
197196
*
198197
* Only standard WebSocket OpCodes can be used as a "default" to use anything other then the
199-
* standard OpCodes use the {@link #write(ByteBuffer, byte, boolean)} method.
198+
* standard OpCodes use the {@link #write(ByteBuffer, WebSocketOpCode, boolean)} method.
200199
*
201200
* @param wsoc the default {@link WebSocketOpCode} to use on this connection.
202201
*/

gradle.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
group = org.threadly
2-
version = 0.16
3-
threadlyVersion = 5.16
2+
version = 0.17
3+
threadlyVersion = 5.18
44
litesocketsVersion = 4.3
55
org.gradle.parallel=true

protocol/src/main/java/org/threadly/litesockets/protocols/http/request/HTTPRequestBuilder.java

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
import java.net.URL;
44
import java.nio.ByteBuffer;
55
import java.nio.charset.Charset;
6+
import java.util.ArrayList;
67
import java.util.HashMap;
8+
import java.util.List;
79
import java.util.Map;
810
import java.util.Map.Entry;
911
import java.util.TreeMap;
@@ -152,8 +154,8 @@ public HTTPRequestBuilder setQueryString(final String query) {
152154
* @return the current {@link HTTPRequestBuilder} object.
153155
*/
154156
public HTTPRequestBuilder appendQuery(final String key, final String value) {
155-
HashMap<String, String> map = new HashMap<String, String>(request.getRequestQuery());
156-
map.put(key, value);
157+
HashMap<String, List<String>> map = new HashMap<>(request.getRequestQuery());
158+
map.computeIfAbsent(key, (ignored) -> new ArrayList<>(1)).add(value);
157159
this.request = new HTTPRequestHeader(request.getRequestMethod(), request.getRequestPath(), map, request.getHttpVersion());
158160
return this;
159161
}
@@ -165,7 +167,7 @@ public HTTPRequestBuilder appendQuery(final String key, final String value) {
165167
* @return the current {@link HTTPRequestBuilder} object.
166168
*/
167169
public HTTPRequestBuilder removeQuery(final String key) {
168-
HashMap<String, String> map = new HashMap<String, String>(request.getRequestQuery());
170+
HashMap<String, List<String>> map = new HashMap<>(request.getRequestQuery());
169171
map.remove(key);
170172
this.request = new HTTPRequestHeader(request.getRequestMethod(), request.getRequestPath(), map, request.getHttpVersion());
171173
return this;
@@ -174,7 +176,7 @@ public HTTPRequestBuilder removeQuery(final String key) {
174176

175177
/**
176178
* Sets the {@link HTTPAddress} for this builder. This will add a Host header into the headers of this builder
177-
* when this object it built. This is also used with the {@link #buildHTTPAddress(boolean)} method.
179+
* when this object it built. This is also used with the {@link #buildHTTPAddress()} method.
178180
*
179181
* @param ha the {@link HTTPAddress} to be set.
180182
* @return the current {@link HTTPRequestBuilder} object.
@@ -187,7 +189,7 @@ public HTTPRequestBuilder setHTTPAddress(final HTTPAddress ha) {
187189
}
188190

189191
/**
190-
* Sets the Host: header in the client. This is also used with the {@link #buildHTTPAddress(boolean)} method.
192+
* Sets the Host: header in the client. This is also used with the {@link #buildHTTPAddress()} method.
191193
* Setting to null will remove this header.
192194
*
193195
*
@@ -204,14 +206,19 @@ public HTTPRequestBuilder setHost(final String host) {
204206
return this;
205207
}
206208

209+
/**
210+
* Sets if the request should be made using ssl or not.
211+
*
212+
* @param doSSL {@code true} if ssl should be used.
213+
* @return the current {@link HTTPRequestBuilder} object.
214+
*/
207215
public HTTPRequestBuilder setSSL(final boolean doSSL) {
208216
this.doSSL = doSSL;
209217
return this;
210218
}
211219

212-
213220
/**
214-
* This sets the port to use in the {@link #buildHTTPAddress(boolean)} method. If not set the default port
221+
* This sets the port to use in the {@link #buildHTTPAddress()} method. If not set the default port
215222
* for the protocol type (http or https) will be used.
216223
*
217224
* @param port port number to set.

protocol/src/main/java/org/threadly/litesockets/protocols/http/request/HTTPRequestHeader.java

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.nio.ByteBuffer;
44
import java.util.Collections;
55
import java.util.LinkedHashMap;
6+
import java.util.List;
67
import java.util.Map;
78

89
import org.threadly.litesockets.protocols.http.shared.HTTPConstants;
@@ -19,7 +20,7 @@ public class HTTPRequestHeader {
1920
private final String rawRequest;
2021
private final String requestMethod;
2122
private final String requestPath;
22-
private final Map<String, String> requestQuery;
23+
private final Map<String, List<String>> requestQuery;
2324
private final String httpVersion;
2425

2526
/**
@@ -60,7 +61,7 @@ public HTTPRequestHeader(final String requestHeader) {
6061
* @param httpVersion the httpVersion to set.
6162
*/
6263
public HTTPRequestHeader(HTTPRequestMethod requestMethod, String requestPath,
63-
Map<String, String> requestQuery, String httpVersion){
64+
Map<String, List<String>> requestQuery, String httpVersion){
6465
this(requestMethod.toString(), requestPath, requestQuery, httpVersion);
6566
}
6667

@@ -74,9 +75,9 @@ public HTTPRequestHeader(HTTPRequestMethod requestMethod, String requestPath,
7475
* @param httpVersion the httpVersion to set.
7576
*/
7677
public HTTPRequestHeader(String requestMethod, String requestPath,
77-
Map<String, String> requestQuery, String httpVersion){
78+
Map<String, List<String>> requestQuery, String httpVersion){ // TODO
7879
this.requestMethod = requestMethod;
79-
final LinkedHashMap<String, String> rqm = new LinkedHashMap<>();
80+
final LinkedHashMap<String, List<String>> rqm = new LinkedHashMap<>();
8081
int queryParamPos = requestPath.indexOf("?");
8182
if(queryParamPos >= 0) {
8283
this.requestPath = requestPath.substring(0, queryParamPos);
@@ -132,10 +133,27 @@ public String getRequestPath() {
132133
*
133134
* @return the request query.
134135
*/
135-
public Map<String, String> getRequestQuery() {
136+
public Map<String, List<String>> getRequestQuery() {
136137
return requestQuery;
137138
}
138139

140+
/**
141+
* Gets the value to a given query parameter. This will throw an exception if there is multiple
142+
* values associated to the key.
143+
*
144+
* @return the request parameter value or {@code null} if none is associated
145+
*/
146+
public String getRequestQueryValue(String paramKey) {
147+
List<String> values = requestQuery.get(paramKey);
148+
if (values == null || values.isEmpty()) {
149+
return null;
150+
} else if (values.size() > 1) {
151+
throw new IllegalStateException("Multiple values for parameter: " + paramKey);
152+
} else {
153+
return values.get(0);
154+
}
155+
}
156+
139157
/**
140158
* Gets the http version.
141159
*

protocol/src/main/java/org/threadly/litesockets/protocols/http/shared/HTTPUtils.java

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package org.threadly.litesockets.protocols.http.shared;
22

33
import java.nio.ByteBuffer;
4+
import java.util.ArrayList;
45
import java.util.Collections;
56
import java.util.HashMap;
7+
import java.util.List;
68
import java.util.Map;
79

810
import org.threadly.litesockets.buffers.MergedByteBuffers;
@@ -51,32 +53,40 @@ public static ByteBuffer wrapInChunk(ByteBuffer bb) {
5153
return newBB;
5254
}
5355

54-
public static String queryToString(Map<String,String> map) {
56+
public static String queryToString(Map<String, List<String>> map) {
5557
if(map.isEmpty()) {
5658
return "";
5759
}
5860

5961
StringBuilder sb = new StringBuilder();
6062
sb.append('?');
61-
for(String k: map.keySet()) {
62-
if(sb.length() > 1) {
63-
sb.append('&');
64-
}
65-
sb.append(k);
66-
String v = map.get(k);
67-
if(! StringUtils.isNullOrEmpty(v)) {
68-
sb.append('=');
69-
sb.append(v);
63+
for(Map.Entry<String, List<String>> e : map.entrySet()) {
64+
if (e.getValue() == null || e.getValue().isEmpty()) {
65+
if(sb.length() > 1) {
66+
sb.append('&');
67+
}
68+
sb.append(e.getKey());
69+
} else {
70+
for (String v : e.getValue()) {
71+
if(sb.length() > 1) {
72+
sb.append('&');
73+
}
74+
sb.append(e.getKey());
75+
if(! StringUtils.isNullOrEmpty(v)) {
76+
sb.append('=');
77+
sb.append(v);
78+
}
79+
}
7080
}
7181
}
7282
return sb.toString();
7383
}
7484

75-
public static Map<String, String> queryToMap(String query) {
85+
public static Map<String, List<String>> queryToMap(String query) {
7686
if (StringUtils.isNullOrEmpty(query)) {
7787
return Collections.emptyMap();
7888
}
79-
Map<String, String> map = new HashMap<>();
89+
Map<String, List<String>> map = new HashMap<>();
8090
if(query.startsWith("?")) {
8191
query = query.substring(1);
8292
} else if (query.contains("?")){
@@ -90,10 +100,11 @@ public static Map<String, String> queryToMap(String query) {
90100
// case where either no `=` or empty key string
91101
continue;
92102
}
103+
List<String> paramValues = map.computeIfAbsent(tmpkv[0], (ignored) -> new ArrayList<>(1));
93104
if(tmpkv.length == 1) {
94-
map.put(tmpkv[0].trim(), "");
105+
paramValues.add("");
95106
} else {
96-
map.put(tmpkv[0].trim(), tmpkv[1].trim());
107+
paramValues.add(tmpkv[1].trim());
97108
}
98109
}
99110
return Collections.unmodifiableMap(map);

0 commit comments

Comments
 (0)