Skip to content

Commit b6af2a0

Browse files
committed
feat: Expose query params on Request (queryParams, queryParam, rawQuery)
1 parent ad6ec3f commit b6af2a0

2 files changed

Lines changed: 75 additions & 0 deletions

File tree

src/main/java/com/retailsvc/http/Request.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
import com.retailsvc.http.internal.DefaultResponseBuilder;
44
import com.sun.net.httpserver.HttpExchange;
5+
import java.net.URLDecoder;
6+
import java.nio.charset.StandardCharsets;
7+
import java.util.LinkedHashMap;
58
import java.util.Map;
69

710
/**
@@ -16,6 +19,7 @@ public final class Request {
1619
private final String operationId;
1720
private final Map<String, String> pathParameters;
1821
private final Map<String, TypeMapper> bodyMappers;
22+
private Map<String, String> queryParamCache;
1923
private boolean responseSent;
2024

2125
public Request(
@@ -53,6 +57,49 @@ public String header(String name) {
5357
return exchange.getRequestHeaders().getFirst(name);
5458
}
5559

60+
/**
61+
* Raw (percent-encoded) query string from the request URI, or {@code null} if the URI has no
62+
* query component.
63+
*/
64+
public String rawQuery() {
65+
return exchange.getRequestURI().getRawQuery();
66+
}
67+
68+
/**
69+
* Decoded query parameters keyed by name. Empty if the URI has no query. For repeated keys, the
70+
* first occurrence wins. Values are URL-decoded with UTF-8.
71+
*/
72+
public Map<String, String> queryParams() {
73+
if (queryParamCache == null) {
74+
queryParamCache = parseQuery(rawQuery());
75+
}
76+
return queryParamCache;
77+
}
78+
79+
/** First decoded value for {@code name}, or {@code null} if absent. */
80+
public String queryParam(String name) {
81+
return queryParams().get(name);
82+
}
83+
84+
private static Map<String, String> parseQuery(String query) {
85+
if (query == null || query.isBlank()) {
86+
return Map.of();
87+
}
88+
Map<String, String> out = new LinkedHashMap<>();
89+
for (String pair : query.split("&")) {
90+
if (pair.isEmpty()) {
91+
continue;
92+
}
93+
int eq = pair.indexOf('=');
94+
String rawKey = eq < 0 ? pair : pair.substring(0, eq);
95+
String rawValue = eq < 0 ? "" : pair.substring(eq + 1);
96+
out.putIfAbsent(
97+
URLDecoder.decode(rawKey, StandardCharsets.UTF_8),
98+
URLDecoder.decode(rawValue, StandardCharsets.UTF_8));
99+
}
100+
return out;
101+
}
102+
56103
public ResponseBuilder respond(int status) {
57104
if (responseSent) {
58105
throw new IllegalStateException("Response already sent");

src/test/java/com/retailsvc/http/RequestTest.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import com.retailsvc.http.internal.DispatchHandler;
99
import com.sun.net.httpserver.Headers;
1010
import com.sun.net.httpserver.HttpExchange;
11+
import java.net.URI;
1112
import java.util.Map;
1213
import java.util.concurrent.atomic.AtomicReference;
1314
import org.junit.jupiter.api.Test;
@@ -48,6 +49,33 @@ void readsBoundContext() throws Exception {
4849
assertThat(seenPathParams.get()).containsEntry("id", "42");
4950
}
5051

52+
@Test
53+
void exposesQueryParams() {
54+
HttpExchange exchange = mock(HttpExchange.class);
55+
when(exchange.getRequestURI())
56+
.thenReturn(URI.create("http://h/x?name=Alice%20Smith&active=true&active=false"));
57+
Request req = new Request(exchange, new byte[0], null, "op", Map.of(), Map.of());
58+
59+
assertThat(req.rawQuery()).isEqualTo("name=Alice%20Smith&active=true&active=false");
60+
assertThat(req.queryParam("name")).isEqualTo("Alice Smith");
61+
assertThat(req.queryParam("active")).isEqualTo("true");
62+
assertThat(req.queryParam("missing")).isNull();
63+
assertThat(req.queryParams())
64+
.containsEntry("name", "Alice Smith")
65+
.containsEntry("active", "true");
66+
}
67+
68+
@Test
69+
void queryParamsEmptyWhenNoQuery() {
70+
HttpExchange exchange = mock(HttpExchange.class);
71+
when(exchange.getRequestURI()).thenReturn(URI.create("http://h/x"));
72+
Request req = new Request(exchange, new byte[0], null, "op", Map.of(), Map.of());
73+
74+
assertThat(req.rawQuery()).isNull();
75+
assertThat(req.queryParams()).isEmpty();
76+
assertThat(req.queryParam("anything")).isNull();
77+
}
78+
5179
@Test
5280
void respondAfterTerminalThrows() throws Exception {
5381
HttpExchange exchange = mock(HttpExchange.class);

0 commit comments

Comments
 (0)