|
| 1 | +/* |
| 2 | + * Copyright (c) 2026 dexpace and Omar Aljarrah |
| 3 | + * |
| 4 | + * Licensed under the MIT License. See LICENSE in the project root. |
| 5 | + * SPDX-License-Identifier: MIT |
| 6 | + */ |
| 7 | + |
| 8 | +package org.dexpace.sdk.core.http.response |
| 9 | + |
| 10 | +import org.dexpace.sdk.core.http.common.Headers |
| 11 | +import org.dexpace.sdk.core.http.common.Protocol |
| 12 | +import org.dexpace.sdk.core.http.request.Request |
| 13 | +import java.io.Closeable |
| 14 | +import java.io.IOException |
| 15 | +import java.util.concurrent.locks.ReentrantLock |
| 16 | +import kotlin.concurrent.withLock |
| 17 | + |
| 18 | +/** |
| 19 | + * Pairs a raw [Response] with a [ResponseHandler] so the typed value can be parsed **lazily and |
| 20 | + * exactly once**, while the raw status / headers / metadata stay readable without forcing |
| 21 | + * deserialization. |
| 22 | + * |
| 23 | + * This is the raw-vs-parsed seam: header and status access (e.g. reading `ETag`, branching on a |
| 24 | + * `404`) goes straight to the underlying response and never touches the body, whereas the typed |
| 25 | + * value is produced on demand by the handler. Because the body is single-use, [value] memoizes |
| 26 | + * the handler's outcome — the first call runs the handler and every subsequent call returns the |
| 27 | + * same result (or re-throws the same failure) without re-invoking the handler or re-reading the |
| 28 | + * body. |
| 29 | + * |
| 30 | + * ## Body consumption |
| 31 | + * |
| 32 | + * The handler owns the body. Calling [value] runs the handler, which typically consumes and |
| 33 | + * closes the body (the built-in [ResponseHandler.string] / [ResponseHandler.empty] and adapter |
| 34 | + * JSON handlers do). **Read any raw headers / status before the first [value] call**, since the |
| 35 | + * body is gone afterwards. [close] is available for the path where the typed value is never |
| 36 | + * needed and the body must still be released. |
| 37 | + * |
| 38 | + * ## Thread-safety |
| 39 | + * |
| 40 | + * Raw accessors are immutable and safe to share. [value] is guarded by a [ReentrantLock] |
| 41 | + * (`synchronized` would pin a carrier thread under virtual threads): concurrent first calls block |
| 42 | + * until the single parse completes and then all observe the same memoized result. A `null` result |
| 43 | + * and a thrown failure are both memoized, so neither triggers a re-parse. |
| 44 | + * |
| 45 | + * @param T The typed value the handler produces. |
| 46 | + * @param raw The underlying raw response. Header / status / metadata access reads from here. |
| 47 | + * @param handler Strategy that maps [raw] to the typed value on first [value] access. |
| 48 | + */ |
| 49 | +public class ParsedResponse<out T> internal constructor( |
| 50 | + public val raw: Response, |
| 51 | + private val handler: ResponseHandler<T>, |
| 52 | +) : Closeable { |
| 53 | + private val lock = ReentrantLock() |
| 54 | + |
| 55 | + // Holds the memoized outcome once the handler has run. A non-null holder means "parsed" |
| 56 | + // (success or failure); the wrapped value distinguishes the two. A holder (rather than a |
| 57 | + // bare value) lets a legitimately-null success memoize without being mistaken for "unparsed". |
| 58 | + @Volatile |
| 59 | + private var outcome: Outcome<T>? = null |
| 60 | + |
| 61 | + /** The request that produced [raw]. Does not parse. */ |
| 62 | + public val request: Request get() = raw.request |
| 63 | + |
| 64 | + /** The negotiated wire protocol. Does not parse. */ |
| 65 | + public val protocol: Protocol get() = raw.protocol |
| 66 | + |
| 67 | + /** The HTTP status. Does not parse. */ |
| 68 | + public val status: Status get() = raw.status |
| 69 | + |
| 70 | + /** The status-line reason phrase, or `null` if absent. Does not parse. */ |
| 71 | + public val message: String? get() = raw.message |
| 72 | + |
| 73 | + /** The response headers. Does not parse. */ |
| 74 | + public val headers: Headers get() = raw.headers |
| 75 | + |
| 76 | + /** |
| 77 | + * Returns the typed value, parsing it on the first call and memoizing the outcome. |
| 78 | + * |
| 79 | + * The handler runs at most once: the first call invokes [ResponseHandler.handle] (which |
| 80 | + * typically consumes and closes the body); subsequent calls return the same value, or |
| 81 | + * re-throw the same failure, without re-running the handler. |
| 82 | + * |
| 83 | + * Any failure the handler throws is memoized and re-thrown verbatim on every later call — not |
| 84 | + * just [IOException]. Handlers commonly throw **unchecked** exceptions (the Jackson `jsonHandler` |
| 85 | + * throws `SerdeException`), so callers should not assume the only escape is [IOException]. |
| 86 | + * |
| 87 | + * @return The parsed value (which may be `null` if the handler is typed `ResponseHandler<T?>` |
| 88 | + * and produces `null`). |
| 89 | + * @throws IOException If the handler failed with an [IOException] — cached and re-thrown. The |
| 90 | + * `@Throws` declaration covers only the checked surface for Java callers; the handler may also |
| 91 | + * propagate **unchecked** exceptions (e.g. `SerdeException` from the Jackson `jsonHandler`), |
| 92 | + * which are memoized and re-thrown the same way. |
| 93 | + */ |
| 94 | + @Throws(IOException::class) |
| 95 | + public fun value(): T { |
| 96 | + outcome?.let { return it.get() } |
| 97 | + return lock.withLock { |
| 98 | + outcome?.let { return it.get() } |
| 99 | + // Memoize the handler's outcome — success or failure — so neither re-runs the handler |
| 100 | + // nor re-reads the (now consumed) body on a subsequent call. |
| 101 | + val resolved: Outcome<T> = |
| 102 | + try { |
| 103 | + Outcome.Success(handler.handle(raw)) |
| 104 | + } catch (t: Throwable) { |
| 105 | + // Catch Throwable, not Exception, on purpose: once the handler has touched the |
| 106 | + // single-use body, re-running it would read an already-consumed stream. Even an |
| 107 | + // Error (e.g. an OOM mid-parse) is memoized so a later call re-throws it rather |
| 108 | + // than re-reading the body and masking the original failure. |
| 109 | + Outcome.Failure(t) |
| 110 | + } |
| 111 | + outcome = resolved |
| 112 | + resolved.get() |
| 113 | + } |
| 114 | + } |
| 115 | + |
| 116 | + /** |
| 117 | + * Releases the raw response body. Idempotent (forwards to [Response.close], which is itself |
| 118 | + * idempotent). Safe to call whether or not [value] has run. |
| 119 | + * |
| 120 | + * @throws IOException If the underlying close fails. |
| 121 | + */ |
| 122 | + @Throws(IOException::class) |
| 123 | + override fun close() { |
| 124 | + raw.close() |
| 125 | + } |
| 126 | + |
| 127 | + private sealed class Outcome<out T> { |
| 128 | + abstract fun get(): T |
| 129 | + |
| 130 | + class Success<out T>(private val value: T) : Outcome<T>() { |
| 131 | + override fun get(): T = value |
| 132 | + } |
| 133 | + |
| 134 | + class Failure(private val error: Throwable) : Outcome<Nothing>() { |
| 135 | + override fun get(): Nothing = throw error |
| 136 | + } |
| 137 | + } |
| 138 | + |
| 139 | + public companion object { |
| 140 | + /** |
| 141 | + * Creates a [ParsedResponse] that parses [response] with [handler] on first access. |
| 142 | + * Java-friendly factory mirroring the Kotlin [Response.parsedWith] extension. |
| 143 | + * |
| 144 | + * @param response The raw response. |
| 145 | + * @param handler Strategy that maps the response to the typed value. |
| 146 | + * @return A lazily-parsing [ParsedResponse]. |
| 147 | + */ |
| 148 | + @JvmStatic |
| 149 | + public fun <T> of( |
| 150 | + response: Response, |
| 151 | + handler: ResponseHandler<T>, |
| 152 | + ): ParsedResponse<T> = ParsedResponse(response, handler) |
| 153 | + } |
| 154 | +} |
| 155 | + |
| 156 | +/** |
| 157 | + * Wraps this response in a [ParsedResponse] bound to [handler], so the typed value parses lazily |
| 158 | + * and exactly once while raw status / headers stay accessible. Kotlin-ergonomic mirror of |
| 159 | + * [ParsedResponse.of]. |
| 160 | + * |
| 161 | + * @param handler Strategy that maps this response to the typed value. |
| 162 | + * @return A lazily-parsing [ParsedResponse]. |
| 163 | + */ |
| 164 | +public fun <T> Response.parsedWith(handler: ResponseHandler<T>): ParsedResponse<T> = ParsedResponse(this, handler) |
0 commit comments