|
2 | 2 |
|
3 | 3 | import java.net.URLDecoder; |
4 | 4 | import java.nio.charset.StandardCharsets; |
| 5 | +import java.util.ArrayList; |
5 | 6 | import java.util.LinkedHashMap; |
| 7 | +import java.util.List; |
6 | 8 | import java.util.Map; |
7 | 9 | import java.util.Objects; |
8 | 10 | import java.util.Optional; |
@@ -31,6 +33,7 @@ public final class Request { |
31 | 33 | private final UnaryOperator<String> headerLookup; |
32 | 34 | private final Map<String, Object> principals; |
33 | 35 | private Map<String, String> queryParamCache; |
| 36 | + private final List<Runnable> afterHooks; |
34 | 37 |
|
35 | 38 | /** |
36 | 39 | * Builds a {@code Request} from transport-neutral primitives. Adapters call this; handlers |
@@ -90,6 +93,32 @@ public Request( |
90 | 93 | this.rawQuery = rawQuery; |
91 | 94 | this.headerLookup = headerLookup; |
92 | 95 | this.principals = Map.copyOf(principals); |
| 96 | + this.afterHooks = new ArrayList<>(); |
| 97 | + } |
| 98 | + |
| 99 | + // Package-private: lets withPrincipals(...) thread the after-hook queue through so that |
| 100 | + // runnables registered on either the original Request or the principals-enriched copy |
| 101 | + // land in the same backing list. |
| 102 | + @SuppressWarnings("java:S107") |
| 103 | + Request( |
| 104 | + byte[] body, |
| 105 | + Object parsed, |
| 106 | + TypeMapper bodyMapper, |
| 107 | + String operationId, |
| 108 | + Map<String, String> pathParameters, |
| 109 | + String rawQuery, |
| 110 | + UnaryOperator<String> headerLookup, |
| 111 | + Map<String, Object> principals, |
| 112 | + List<Runnable> afterHooks) { |
| 113 | + this.body = body; |
| 114 | + this.parsed = parsed; |
| 115 | + this.bodyMapper = bodyMapper; |
| 116 | + this.operationId = operationId; |
| 117 | + this.pathParameters = pathParameters; |
| 118 | + this.rawQuery = rawQuery; |
| 119 | + this.headerLookup = headerLookup; |
| 120 | + this.principals = Map.copyOf(principals); |
| 121 | + this.afterHooks = afterHooks; |
93 | 122 | } |
94 | 123 |
|
95 | 124 | public byte[] bytes() { |
@@ -214,7 +243,35 @@ public Optional<Object> principal(String schemeName) { |
214 | 243 | */ |
215 | 244 | public Request withPrincipals(Map<String, Object> principals) { |
216 | 245 | return new Request( |
217 | | - body, parsed, bodyMapper, operationId, pathParameters, rawQuery, headerLookup, principals); |
| 246 | + body, |
| 247 | + parsed, |
| 248 | + bodyMapper, |
| 249 | + operationId, |
| 250 | + pathParameters, |
| 251 | + rawQuery, |
| 252 | + headerLookup, |
| 253 | + principals, |
| 254 | + afterHooks); |
| 255 | + } |
| 256 | + |
| 257 | + /** |
| 258 | + * Queues a {@link Runnable} to execute after the HTTP response has been sent to the client. Runs |
| 259 | + * on the request thread inside the library's request {@link ScopedValue} binding. Multiple calls |
| 260 | + * queue FIFO. Exceptions thrown by the runnable are logged at DEBUG and swallowed. |
| 261 | + * |
| 262 | + * <p>Calls made after the runner has snapshotted the queue (e.g. from inside a running hook, or |
| 263 | + * from a leaked {@code Request} reference held past the response) are silently ignored. |
| 264 | + * |
| 265 | + * @throws NullPointerException if {@code runnable} is null |
| 266 | + */ |
| 267 | + public void afterResponse(Runnable runnable) { |
| 268 | + Objects.requireNonNull(runnable, "runnable must not be null"); |
| 269 | + afterHooks.add(runnable); |
| 270 | + } |
| 271 | + |
| 272 | + /** Package-private accessor for the after-hook queue; used by RequestPreparationFilter. */ |
| 273 | + List<Runnable> afterHooks() { |
| 274 | + return afterHooks; |
218 | 275 | } |
219 | 276 |
|
220 | 277 | private static Map<String, String> parseQuery(String query) { |
|
0 commit comments