|
3 | 3 | import com.retailsvc.http.spec.HttpMethod; |
4 | 4 | import java.net.URLDecoder; |
5 | 5 | import java.nio.charset.StandardCharsets; |
| 6 | +import java.util.ArrayList; |
6 | 7 | import java.util.LinkedHashMap; |
| 8 | +import java.util.List; |
7 | 9 | import java.util.Map; |
8 | 10 | import java.util.Objects; |
9 | 11 | import java.util.Optional; |
@@ -33,6 +35,7 @@ public final class Request { |
33 | 35 | private final UnaryOperator<String> headerLookup; |
34 | 36 | private final Map<String, Object> principals; |
35 | 37 | private Map<String, String> queryParamCache; |
| 38 | + private final List<Runnable> afterHooks; |
36 | 39 |
|
37 | 40 | /** |
38 | 41 | * Builds a {@code Request} from transport-neutral primitives. Adapters call this; handlers |
@@ -145,6 +148,34 @@ public Request( |
145 | 148 | this.method = method; |
146 | 149 | this.headerLookup = headerLookup; |
147 | 150 | this.principals = Map.copyOf(principals); |
| 151 | + this.afterHooks = new ArrayList<>(); |
| 152 | + } |
| 153 | + |
| 154 | + // Package-private: lets withPrincipals(...) thread the after-hook queue through so that |
| 155 | + // runnables registered on either the original Request or the principals-enriched copy |
| 156 | + // land in the same backing list. |
| 157 | + @SuppressWarnings("java:S107") |
| 158 | + Request( |
| 159 | + byte[] body, |
| 160 | + Object parsed, |
| 161 | + TypeMapper bodyMapper, |
| 162 | + String operationId, |
| 163 | + Map<String, String> pathParameters, |
| 164 | + String rawQuery, |
| 165 | + UnaryOperator<String> headerLookup, |
| 166 | + Map<String, Object> principals, |
| 167 | + HttpMethod method, |
| 168 | + List<Runnable> afterHooks) { |
| 169 | + this.body = body; |
| 170 | + this.parsed = parsed; |
| 171 | + this.bodyMapper = bodyMapper; |
| 172 | + this.operationId = operationId; |
| 173 | + this.pathParameters = pathParameters; |
| 174 | + this.rawQuery = rawQuery; |
| 175 | + this.method = method; |
| 176 | + this.headerLookup = headerLookup; |
| 177 | + this.principals = Map.copyOf(principals); |
| 178 | + this.afterHooks = afterHooks; |
148 | 179 | } |
149 | 180 |
|
150 | 181 | public byte[] bytes() { |
@@ -286,7 +317,28 @@ public Request withPrincipals(Map<String, Object> principals) { |
286 | 317 | rawQuery, |
287 | 318 | headerLookup, |
288 | 319 | principals, |
289 | | - method); |
| 320 | + method, |
| 321 | + afterHooks); |
| 322 | + } |
| 323 | + |
| 324 | + /** |
| 325 | + * Queues a {@link Runnable} to execute after the HTTP response has been sent to the client. Runs |
| 326 | + * on the request thread inside the library's request {@link ScopedValue} binding. Multiple calls |
| 327 | + * queue FIFO. Exceptions thrown by the runnable are logged at DEBUG and swallowed. |
| 328 | + * |
| 329 | + * <p>Calls made after the runner has snapshotted the queue (e.g. from inside a running hook, or |
| 330 | + * from a leaked {@code Request} reference held past the response) are silently ignored. |
| 331 | + * |
| 332 | + * @throws NullPointerException if {@code runnable} is null |
| 333 | + */ |
| 334 | + public void afterResponse(Runnable runnable) { |
| 335 | + Objects.requireNonNull(runnable, "runnable must not be null"); |
| 336 | + afterHooks.add(runnable); |
| 337 | + } |
| 338 | + |
| 339 | + /** Package-private accessor for the after-hook queue; used by RequestPreparationFilter. */ |
| 340 | + List<Runnable> afterHooks() { |
| 341 | + return afterHooks; |
290 | 342 | } |
291 | 343 |
|
292 | 344 | private static Map<String, String> parseQuery(String query) { |
|
0 commit comments