-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathRequest.java
More file actions
346 lines (321 loc) · 12.7 KB
/
Copy pathRequest.java
File metadata and controls
346 lines (321 loc) · 12.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
package com.retailsvc.http;
import com.retailsvc.http.internal.QueryParams;
import com.retailsvc.http.spec.HttpMethod;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.UnaryOperator;
/**
* Read-only per-request handle passed to {@link RequestHandler}. Carries the HTTP method, parsed
* body, path parameters, query parameters, headers, and operation ID.
*
* <p>{@code Request} is transport-neutral: it holds the body bytes, the raw query string, the path
* parameter map, and a header lookup function. The transport adapter (today the built-in JDK {@code
* HttpServer}, tomorrow potentially Netty or another backend) is responsible for extracting those
* primitives from its own request representation. Handlers consume a {@code Request} and return a
* {@link Response}.
*/
public final class Request {
private static final String CONTENT_TYPE = "Content-Type";
private final byte[] body;
private final Object parsed;
private final TypeMapper bodyMapper;
private final String operationId;
private final Map<String, String> pathParameters;
private final String rawQuery;
private final HttpMethod method;
private final UnaryOperator<String> headerLookup;
private final Map<String, Object> principals;
private Map<String, String> queryParamCache;
private final List<Runnable> afterHooks;
/**
* Builds a {@code Request} from transport-neutral primitives. Adapters call this; handlers
* receive the constructed instance.
*
* <p>{@code method} is {@code null}; use the 9-arg constructor to supply it.
*
* @param body raw request body bytes; never {@code null}, may be empty
* @param parsed loose structural view of the body (Map / List / boxed primitive), or {@code null}
* @param bodyMapper {@link TypeMapper} that produced {@code parsed}, used for typed conversion;
* may be {@code null} if there is no body
* @param operationId the OpenAPI {@code operationId} the request was routed to
* @param pathParameters path variables extracted by the router
* @param rawQuery raw (percent-encoded) query string, or {@code null} if absent
* @param headerLookup first-value, case-insensitive header lookup; returns {@code null} if absent
*/
public Request(
byte[] body,
Object parsed,
TypeMapper bodyMapper,
String operationId,
Map<String, String> pathParameters,
String rawQuery,
UnaryOperator<String> headerLookup) {
this(
body,
parsed,
bodyMapper,
operationId,
pathParameters,
rawQuery,
headerLookup,
Map.of(),
null);
}
/**
* Builds a {@code Request} from transport-neutral primitives with an explicit principals map.
*
* <p>{@code method} is {@code null}; use the 9-arg constructor to supply it.
*
* @param body raw request body bytes; never {@code null}, may be empty
* @param parsed loose structural view of the body (Map / List / boxed primitive), or {@code null}
* @param bodyMapper {@link TypeMapper} that produced {@code parsed}, used for typed conversion;
* may be {@code null} if there is no body
* @param operationId the OpenAPI {@code operationId} the request was routed to
* @param pathParameters path variables extracted by the router
* @param rawQuery raw (percent-encoded) query string, or {@code null} if absent
* @param headerLookup first-value, case-insensitive header lookup; returns {@code null} if absent
* @param principals principals stashed by the security filter, keyed by scheme name
*/
@SuppressWarnings("java:S107")
public Request(
byte[] body,
Object parsed,
TypeMapper bodyMapper,
String operationId,
Map<String, String> pathParameters,
String rawQuery,
UnaryOperator<String> headerLookup,
Map<String, Object> principals) {
this(
body,
parsed,
bodyMapper,
operationId,
pathParameters,
rawQuery,
headerLookup,
principals,
null);
}
/**
* Builds a {@code Request} from transport-neutral primitives with explicit principals and method.
*
* @param body raw request body bytes; never {@code null}, may be empty
* @param parsed loose structural view of the body (Map / List / boxed primitive), or {@code null}
* @param bodyMapper {@link TypeMapper} that produced {@code parsed}, used for typed conversion;
* may be {@code null} if there is no body
* @param operationId the OpenAPI {@code operationId} the request was routed to
* @param pathParameters path variables extracted by the router
* @param rawQuery raw (percent-encoded) query string, or {@code null} if absent
* @param headerLookup first-value, case-insensitive header lookup; returns {@code null} if absent
* @param principals principals stashed by the security filter, keyed by scheme name
* @param method the HTTP method of the request. Never {@code null} when constructed through the
* normal request pipeline. {@code null} only when constructed via the legacy 7- or 8-argument
* constructors (kept for backward compatibility).
*/
// Request is transport-neutral and assembled from primitives at the adapter boundary; collapsing
// these into a holder type would just move the parameter count one level out without simplifying
// the call site, so the 9-arg constructor is preferred over the rule's 7-param limit.
@SuppressWarnings("java:S107")
public Request(
byte[] body,
Object parsed,
TypeMapper bodyMapper,
String operationId,
Map<String, String> pathParameters,
String rawQuery,
UnaryOperator<String> headerLookup,
Map<String, Object> principals,
HttpMethod method) {
this.body = body;
this.parsed = parsed;
this.bodyMapper = bodyMapper;
this.operationId = operationId;
this.pathParameters = pathParameters;
this.rawQuery = rawQuery;
this.method = method;
this.headerLookup = headerLookup;
this.principals = Map.copyOf(principals);
this.afterHooks = new ArrayList<>();
}
// Package-private: lets withPrincipals(...) thread the after-hook queue through so that
// runnables registered on either the original Request or the principals-enriched copy
// land in the same backing list.
@SuppressWarnings("java:S107")
Request(
byte[] body,
Object parsed,
TypeMapper bodyMapper,
String operationId,
Map<String, String> pathParameters,
String rawQuery,
UnaryOperator<String> headerLookup,
Map<String, Object> principals,
HttpMethod method,
List<Runnable> afterHooks) {
this.body = body;
this.parsed = parsed;
this.bodyMapper = bodyMapper;
this.operationId = operationId;
this.pathParameters = pathParameters;
this.rawQuery = rawQuery;
this.method = method;
this.headerLookup = headerLookup;
this.principals = Map.copyOf(principals);
this.afterHooks = afterHooks;
}
public byte[] bytes() {
return body;
}
/**
* Loose structural view of the body (typically a {@code Map} / {@code List} / boxed primitive).
*/
public Object parsed() {
return parsed;
}
/**
* Typed view of the body, deserialised into {@code type} by the request's body mapper.
*
* <p>Requires the registered {@link TypeMapper} for the request's {@code Content-Type} to
* implement {@link TypedTypeMapper} — Jackson does, the built-in form and text mappers do not. If
* the loose {@link #parsed()} value already is an instance of {@code type}, it is returned
* directly without re-deserialising.
*
* @throws NullPointerException if {@code type} is null
* @throws IllegalStateException if there is no body, or if the body mapper does not implement
* {@link TypedTypeMapper}
*/
public <T> T asPojo(Class<T> type) {
Objects.requireNonNull(type, "type must not be null");
if (body == null || body.length == 0) {
throw new IllegalStateException("request has no body");
}
if (parsed != null && type.isInstance(parsed)) {
return type.cast(parsed);
}
String contentType = headerLookup.apply(CONTENT_TYPE);
if (bodyMapper instanceof TypedTypeMapper typed) {
return typed.readAs(body, contentType, type);
}
throw new IllegalStateException(
"body mapper for "
+ contentType
+ " does not support typed conversion; the mapper must implement TypedTypeMapper");
}
/**
* Value of the {@code Content-Type} request header, or {@link Optional#empty()} if absent or
* blank. Convenience for {@code header("Content-Type")} — the most frequently inspected header.
*/
public Optional<String> contentType() {
return header(CONTENT_TYPE);
}
public String operationId() {
return operationId;
}
public Map<String, String> pathParams() {
return pathParameters;
}
/** Value of the path parameter {@code name}, or {@code null} if absent. */
public String pathParam(String name) {
return pathParameters.get(name);
}
/**
* First value of the request header {@code name}, or {@link Optional#empty()} if absent or blank.
* Blank values are treated as missing so callers can write {@code req.header("X").map(...)}
* without the extra {@code filter(v -> !v.isBlank())} step.
*/
public Optional<String> header(String name) {
String raw = headerLookup.apply(name);
return raw == null || raw.isBlank() ? Optional.empty() : Optional.of(raw);
}
/**
* Raw (percent-encoded) query string from the request URI, or {@code null} if the URI has no
* query component.
*/
public String rawQuery() {
return rawQuery;
}
/**
* Decoded query parameters keyed by name. Empty if the URI has no query. For repeated keys, the
* first occurrence wins. Values are URL-decoded with UTF-8.
*/
public Map<String, String> queryParams() {
if (queryParamCache == null) {
queryParamCache = QueryParams.parse(rawQuery);
}
return queryParamCache;
}
/**
* First decoded value for query parameter {@code name}, or {@link Optional#empty()} if absent or
* blank. Blank values are treated as missing so callers can write {@code
* req.queryParam("limit").map(Integer::parseInt).orElse(DEFAULT)} without the extra {@code
* filter(v -> !v.isBlank())} step.
*/
public Optional<String> queryParam(String name) {
String raw = queryParams().get(name);
return raw == null || raw.isBlank() ? Optional.empty() : Optional.of(raw);
}
/**
* Principals stashed by {@code SecurityFilter}, keyed by securityScheme name. Empty when the
* request had no security requirements or when {@code useExternalAuthentication()} is set.
*/
public Map<String, Object> principals() {
return principals;
}
/** Convenience for the common single-scheme case. */
public Optional<Object> principal(String schemeName) {
return Optional.ofNullable(principals.get(schemeName));
}
/**
* HTTP method of the request. Never {@code null} for requests routed through the standard
* pipeline; {@code null} only when the {@code Request} was constructed via a legacy constructor
* without a method.
*/
public HttpMethod method() {
return method;
}
/**
* Returns a new {@code Request} identical to this one except with the supplied principals. Used
* by {@code SecurityFilter} on success; the returned instance carries the principals through to
* the {@link RequestHandler}.
*/
public Request withPrincipals(Map<String, Object> principals) {
return new Request(
body,
parsed,
bodyMapper,
operationId,
pathParameters,
rawQuery,
headerLookup,
principals,
method,
afterHooks);
}
/**
* Queues a {@link Runnable} to execute after the HTTP response has been sent to the client. Runs
* on the request thread inside the library's request {@link ScopedValue} binding. Multiple calls
* queue FIFO. Exceptions thrown by the runnable are logged at DEBUG and swallowed.
*
* <p>Calls made after the runner has snapshotted the queue (e.g. from inside a running hook, or
* from a leaked {@code Request} reference held past the response) are silently ignored.
*
* @throws NullPointerException if {@code runnable} is null
*/
public void afterResponse(Runnable runnable) {
Objects.requireNonNull(runnable, "runnable must not be null");
afterHooks.add(runnable);
}
/**
* Returns an unmodifiable view of the queued after-response runnables. Intended for the framework
* runner; consumers should use {@link #afterResponse(Runnable)} to register runnables rather than
* inspecting this list directly.
*/
public List<Runnable> afterHooks() {
return Collections.unmodifiableList(afterHooks);
}
}