diff --git a/sdk-async-virtualthreads/src/main/kotlin/org/dexpace/sdk/async/virtualthreads/MdcAwareExecutor.kt b/sdk-async-virtualthreads/src/main/kotlin/org/dexpace/sdk/async/virtualthreads/MdcAwareExecutor.kt index 0c34c42e..b51e483d 100644 --- a/sdk-async-virtualthreads/src/main/kotlin/org/dexpace/sdk/async/virtualthreads/MdcAwareExecutor.kt +++ b/sdk-async-virtualthreads/src/main/kotlin/org/dexpace/sdk/async/virtualthreads/MdcAwareExecutor.kt @@ -44,19 +44,23 @@ import java.util.concurrent.TimeUnit * [VirtualThreadAsyncHttpClient] for the `close()` path so shutdown semantics are unchanged. */ internal class MdcAwareExecutor(private val delegate: ExecutorService) : ExecutorService by delegate { + private fun MdcSnapshot.wrap(command: Runnable): Runnable = Runnable { withMdc { command.run() } } + + private fun MdcSnapshot.wrap(task: Callable): Callable = Callable { withMdc { task.call() } } + override fun execute(command: Runnable) { val snapshot = MdcSnapshot.capture() - delegate.execute { snapshot.withMdc { command.run() } } + delegate.execute(snapshot.wrap(command)) } override fun submit(task: Callable): Future { val snapshot = MdcSnapshot.capture() - return delegate.submit(Callable { snapshot.withMdc { task.call() } }) + return delegate.submit(snapshot.wrap(task)) } override fun submit(task: Runnable): Future<*> { val snapshot = MdcSnapshot.capture() - return delegate.submit { snapshot.withMdc { task.run() } } + return delegate.submit(snapshot.wrap(task)) } override fun submit( @@ -64,12 +68,12 @@ internal class MdcAwareExecutor(private val delegate: ExecutorService) : Executo result: T, ): Future { val snapshot = MdcSnapshot.capture() - return delegate.submit({ snapshot.withMdc { task.run() } }, result) + return delegate.submit(snapshot.wrap(task), result) } override fun invokeAll(tasks: MutableCollection>): MutableList> { val snapshot = MdcSnapshot.capture() - return delegate.invokeAll(tasks.map { task -> Callable { snapshot.withMdc { task.call() } } }) + return delegate.invokeAll(tasks.map { task -> snapshot.wrap(task) }) } override fun invokeAll( @@ -78,12 +82,12 @@ internal class MdcAwareExecutor(private val delegate: ExecutorService) : Executo unit: TimeUnit, ): MutableList> { val snapshot = MdcSnapshot.capture() - return delegate.invokeAll(tasks.map { task -> Callable { snapshot.withMdc { task.call() } } }, timeout, unit) + return delegate.invokeAll(tasks.map { task -> snapshot.wrap(task) }, timeout, unit) } override fun invokeAny(tasks: MutableCollection>): T { val snapshot = MdcSnapshot.capture() - return delegate.invokeAny(tasks.map { task -> Callable { snapshot.withMdc { task.call() } } }) + return delegate.invokeAny(tasks.map { task -> snapshot.wrap(task) }) } override fun invokeAny( @@ -92,6 +96,6 @@ internal class MdcAwareExecutor(private val delegate: ExecutorService) : Executo unit: TimeUnit, ): T { val snapshot = MdcSnapshot.capture() - return delegate.invokeAny(tasks.map { task -> Callable { snapshot.withMdc { task.call() } } }, timeout, unit) + return delegate.invokeAny(tasks.map { task -> snapshot.wrap(task) }, timeout, unit) } } diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/Configuration.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/Configuration.kt index 7b6598bc..74066d6d 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/Configuration.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/config/Configuration.kt @@ -202,7 +202,7 @@ public class Configuration internal constructor( // ISO-8601 path: `PT5S`, `P1D`, etc. Reject negative durations (e.g. `PT-5S`) for the // same reason the shorthand path does below — downstream consumers (Clock.sleep, // Futures.delay) assume a non-negative duration and throw on a negative one. - if (Character.toUpperCase(raw[0]) == 'P') { + if (raw[0].uppercaseChar() == 'P') { return try { val d = Duration.parse(raw) if (d.isNegative) null else d diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/auth/AuthChallengeParser.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/auth/AuthChallengeParser.kt index 572891cd..789beb54 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/auth/AuthChallengeParser.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/auth/AuthChallengeParser.kt @@ -290,15 +290,15 @@ public object AuthChallengeParser { } } + private val TOKEN_PUNCTUATION: Set = "!#$%&'*+-.^_`|~".toSet() + + private val TOKEN68_PUNCTUATION: Set = "-._~+/".toSet() + /** RFC 7230 token char: ALPHA / DIGIT / one of the punctuation set. */ private fun isTokenChar(c: Char): Boolean = - (c in 'a'..'z') || (c in 'A'..'Z') || (c in '0'..'9') || - c == '!' || c == '#' || c == '$' || c == '%' || c == '&' || - c == '\'' || c == '*' || c == '+' || c == '-' || c == '.' || - c == '^' || c == '_' || c == '`' || c == '|' || c == '~' + (c in 'a'..'z') || (c in 'A'..'Z') || (c in '0'..'9') || c in TOKEN_PUNCTUATION /** RFC 7235 token68 char (excluding the trailing "=" pad, handled separately). */ private fun isToken68Char(c: Char): Boolean = - (c in 'a'..'z') || (c in 'A'..'Z') || (c in '0'..'9') || - c == '-' || c == '.' || c == '_' || c == '~' || c == '+' || c == '/' + (c in 'a'..'z') || (c in 'A'..'Z') || (c in '0'..'9') || c in TOKEN68_PUNCTUATION } diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/auth/DigestChallengeHandler.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/auth/DigestChallengeHandler.kt index 80e9558f..6285015e 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/auth/DigestChallengeHandler.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/auth/DigestChallengeHandler.kt @@ -96,35 +96,18 @@ public class DigestChallengeHandler * - it carries a `realm` and `nonce`. * * Ordering: we scan [preferredAlgorithms] first, then within that scan the - * challenges. This ensures SHA-256 wins over MD5 even when MD5 appears first + * candidates. This ensures SHA-256 wins over MD5 even when MD5 appears first * in the header. * - * Each `continue` skips a challenge that fails a specific validation gate - * (scheme, realm, nonce, qop, algorithm). Collapsing to a single composite - * predicate would obscure which validation rejected the candidate when - * debugging Digest interop. + * Per-challenge validation is delegated to [toCandidate]; this scan only applies + * the algorithm-priority ordering, independent of the order challenges arrived in. */ - @Suppress("LoopWithTooManyJumpStatements") private fun pickChallenge( challenges: List, ): Pair? { // Find all challenges that match Digest with a satisfiable qop/realm/nonce // — we'll filter by algorithm preference below. - val candidates = ArrayList>(challenges.size) - for (challenge in challenges) { - if (!challenge.scheme.equals("Digest", ignoreCase = true)) continue - if (challenge.parameters["realm"] == null) continue - if (challenge.parameters["nonce"] == null) continue - if (!qopSupportsAuth(challenge.parameters["qop"])) continue - val algorithmName = challenge.parameters["algorithm"] - val algorithm = - if (algorithmName == null) { - DigestAlgorithm.MD5 // RFC 7616 §3.3: MD5 is the default when omitted. - } else { - DigestAlgorithm.fromString(algorithmName) ?: continue - } - candidates.add(challenge to algorithm) - } + val candidates = challenges.mapNotNull(::toCandidate) // Pick the candidate whose algorithm appears earliest in our preference list. // Returns null when no candidate matches any preferred algorithm. for (preferred in preferredAlgorithms) { @@ -133,6 +116,30 @@ public class DigestChallengeHandler return null } + /** + * Maps a single challenge to a satisfiable (challenge, algorithm) candidate, or null + * when it is not usable: it must be a `Digest` challenge carrying `realm`, `nonce`, and + * a supported `qop`. Its algorithm is then resolved — an absent `algorithm` parameter + * defaults to MD5 (RFC 7616 §3.3), and an unsupported one is declined. + */ + private fun toCandidate(challenge: AuthenticateChallenge): Pair? { + val isInvalidChallenge = + !challenge.scheme.equals("Digest", ignoreCase = true) || + challenge.parameters["realm"] == null || + challenge.parameters["nonce"] == null || + !qopSupportsAuth(challenge.parameters["qop"]) + + if (isInvalidChallenge) { + return null + } + + val algorithmName = challenge.parameters["algorithm"] ?: return challenge to DigestAlgorithm.MD5 + + return DigestAlgorithm.fromString(algorithmName)?.let { + challenge to it + } + } + /** * Builds the `Authorization: Digest ...` header value for a single challenge. * Follows RFC 7616 §3.4.6 — HA1, HA2, response — plus the standard parameter diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/common/MediaType.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/common/MediaType.kt index b8453d9a..3f5e9ba2 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/common/MediaType.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/common/MediaType.kt @@ -100,16 +100,16 @@ public data class MediaType private constructor( if (value.isNotEmpty() && value.all(::isTokenChar)) { return value } - val sb = StringBuilder(value.length + 2) - sb.append('"') - value.forEach { ch -> - if (ch == '\\' || ch == '"') { - sb.append('\\') + return buildString(value.length + 2) { + append('"') + value.forEach { ch -> + if (ch == '\\' || ch == '"') { + append('\\') + } + append(ch) } - sb.append(ch) + append('"') } - sb.append('"') - return sb.toString() } /** diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/context/DispatchContext.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/context/DispatchContext.kt index 921a7da6..0e3cde22 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/context/DispatchContext.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/context/DispatchContext.kt @@ -61,7 +61,7 @@ public data class DispatchContext( * counter to it for the actual key. */ private fun deriveCallKey(instrumentationContext: InstrumentationContext): String = - instrumentationContext.traceId.value + ":" + instrumentationContext.spanId.value + "${instrumentationContext.traceId.value}:${instrumentationContext.spanId.value}" /** * A dispatch context with a no-op instrumentation context; used when tracing is @@ -81,6 +81,6 @@ public data class DispatchContext( * [DispatchContext]), so every link in the chain is collision-safe by default. */ internal fun mintCallKey(instrumentationContext: InstrumentationContext): String = - deriveCallKey(instrumentationContext) + ":" + mintCounter.incrementAndGet() + "${deriveCallKey(instrumentationContext)}:${mintCounter.incrementAndGet()}" } } diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/pipeline/AsyncHttpPipelineBuilder.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/pipeline/AsyncHttpPipelineBuilder.kt index 30ae8fe7..faf2878f 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/pipeline/AsyncHttpPipelineBuilder.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/pipeline/AsyncHttpPipelineBuilder.kt @@ -99,10 +99,7 @@ public class AsyncHttpPipelineBuilder(private val httpClient: AsyncHttpClient) { /** Builds an immutable [AsyncHttpPipeline]. */ public fun build(): AsyncHttpPipeline { val ordered = steps.flatten() - val array = arrayOfNulls(ordered.size) - for ((i, s) in ordered.withIndex()) array[i] = s - @Suppress("UNCHECKED_CAST") - return AsyncHttpPipeline(httpClient, array as Array) + return AsyncHttpPipeline(httpClient, Array(ordered.size) { ordered[it] }) } public companion object { diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/pipeline/HttpPipelineBuilder.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/pipeline/HttpPipelineBuilder.kt index e8a6edf4..de2344ba 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/pipeline/HttpPipelineBuilder.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/pipeline/HttpPipelineBuilder.kt @@ -110,16 +110,10 @@ public class HttpPipelineBuilder(private val httpClient: HttpClient) { /** * Builds an immutable [HttpPipeline] in stage order. [Stage.SEND] is reserved for the * transport and is skipped. - * - * `arrayOfNulls` then fill — Kotlin's `List.toTypedArray()` is erased to - * `Array` at runtime which fails the `Array` cast. */ public fun build(): HttpPipeline { val ordered = steps.flatten() - val array = arrayOfNulls(ordered.size) - for ((i, s) in ordered.withIndex()) array[i] = s - @Suppress("UNCHECKED_CAST") - return HttpPipeline(httpClient, array as Array) + return HttpPipeline(httpClient, Array(ordered.size) { ordered[it] }) } public companion object { diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/pipeline/steps/DefaultRedirectStep.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/pipeline/steps/DefaultRedirectStep.kt index 8116025a..dd2fa02f 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/pipeline/steps/DefaultRedirectStep.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/pipeline/steps/DefaultRedirectStep.kt @@ -258,11 +258,9 @@ public open class DefaultRedirectStep // underlying store may return names in mixed case (`Content-Type`), so lower-case // before the prefix test. Iterate a snapshot of the keys to avoid concurrent // modification while mutating the builder. - val toRemove = ArrayList() - for (name in headers.names()) { - if (name.lowercase(Locale.US).startsWith("content-")) toRemove.add(name) - } - for (name in toRemove) builder.remove(name) + headers.names() + .filter { it.lowercase(Locale.US).startsWith("content-") } + .forEach { builder.remove(it) } return builder.build() } diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/response/LoggableResponseBody.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/response/LoggableResponseBody.kt index 027d8ec1..61d88a09 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/response/LoggableResponseBody.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/response/LoggableResponseBody.kt @@ -122,8 +122,7 @@ public class LoggableResponseBody * otherwise the delegate's reported length (the true length), since the capture is just * a bounded prefix. */ - override fun contentLength(): Long = - if (fullyCaptured) captured?.size ?: delegate.contentLength() else delegate.contentLength() + override fun contentLength(): Long = (if (fullyCaptured) captured?.size else null) ?: delegate.contentLength() /** * Returns a view of the captured body. Drains (up to the cap) on first call. If the drain diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/instrumentation/SpanLoggingExtensions.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/instrumentation/SpanLoggingExtensions.kt index 96c86d50..de217bf2 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/instrumentation/SpanLoggingExtensions.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/instrumentation/SpanLoggingExtensions.kt @@ -45,14 +45,25 @@ public fun Span.makeCurrentWithLoggingContext(): TracingScope { MDC.put(MDC_SPAN_ID, spanId) return TracingScope { try { - if (prevTraceId == null) MDC.remove(MDC_TRACE_ID) else MDC.put(MDC_TRACE_ID, prevTraceId) - if (prevSpanId == null) MDC.remove(MDC_SPAN_ID) else MDC.put(MDC_SPAN_ID, prevSpanId) + restoreMdc(MDC_TRACE_ID, prevTraceId) + restoreMdc(MDC_SPAN_ID, prevSpanId) } finally { inner.close() } } } +/** + * Restores a single MDC [key] to its [previous] value: removes the key when it was previously + * unset, otherwise re-puts the captured value. + */ +private fun restoreMdc( + key: String, + previous: String?, +) { + if (previous == null) MDC.remove(key) else MDC.put(key, previous) +} + /** SLF4J MDC key for the W3C trace id. Lowercase-dotted to match the SDK's field-naming convention. */ internal const val MDC_TRACE_ID: String = "trace.id" diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/instrumentation/UrlRedactor.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/instrumentation/UrlRedactor.kt index 7c709688..bf20619c 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/instrumentation/UrlRedactor.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/instrumentation/UrlRedactor.kt @@ -89,9 +89,7 @@ public object UrlRedactor { private fun lowercaseAllowList(allowed: Set): Set { if (allowed.isEmpty()) return emptySet() - val lower = HashSet(allowed.size) - for (name in allowed) lower.add(name.lowercase()) - return lower + return allowed.mapTo(HashSet(allowed.size)) { it.lowercase() } } private fun rebuild( diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/io/Io.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/io/Io.kt index d9520a79..a0966007 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/io/Io.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/io/Io.kt @@ -65,13 +65,13 @@ public object Io { public fun installProvider(provider: IoProvider) { lock.withLock { val existing = installed - if (existing != null && existing !== provider) { - throw IllegalStateException( - "An IoProvider (${existing::class.qualifiedName ?: existing::class}) is " + - "already installed; refusing to overwrite with a different provider " + - "(${provider::class.qualifiedName ?: provider::class}). " + - "Use withProvider { ... } from org.dexpace.sdk.core.testing for scoped overrides.", - ) + check(existing == null || existing === provider) { + val existingName = existing!!::class.qualifiedName ?: existing::class + val providerName = provider::class.qualifiedName ?: provider::class + "An IoProvider ($existingName) is " + + "already installed; refusing to overwrite with a different provider " + + "($providerName). " + + "Use withProvider { ... } from org.dexpace.sdk.core.testing for scoped overrides." } installed = provider } diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/io/TeeSink.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/io/TeeSink.kt index 8edc66c9..0af6238c 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/io/TeeSink.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/io/TeeSink.kt @@ -93,13 +93,22 @@ internal class TeeSink( source: Buffer, byteCount: Long, ) { - val allowed = (tapLimit - mirrored).coerceAtLeast(0L) - if (allowed == 0L) return - val copy = if (byteCount < allowed) byteCount else allowed + val copy = tapAllowance(byteCount) + if (copy == 0L) return source.copyTo(tap, 0, copy) mirrored += copy } + /** + * Computes how many of [requested] bytes may still be mirrored into [tap]: the smaller of + * [requested] and the remaining [tapLimit] budget, clamped to never go negative. The actual + * copy and [mirrored] advancement stay at each call site. + */ + private fun tapAllowance(requested: Long): Long { + val remaining = (tapLimit - mirrored).coerceAtLeast(0L) + return if (requested < remaining) requested else remaining + } + @Throws(IOException::class) override fun flush() { primary.flush() @@ -199,11 +208,10 @@ internal class TeeSink( // Tap first (within the budget), primary second (see single-byte overload): a // primary-side failure leaves the failing chunk captured in the tap. The FULL // chunk is always forwarded to the primary so the wire body is never truncated. - val allowed = (tapLimit - mirrored).coerceAtLeast(0L) - if (allowed > 0L) { - val copy = if (len.toLong() < allowed) len else allowed.toInt() - tapStream.write(b, off, copy) - mirrored += copy.toLong() + val copy = tapAllowance(len.toLong()) + if (copy > 0L) { + tapStream.write(b, off, copy.toInt()) + mirrored += copy } primaryStream.write(b, off, len) } diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/pagination/LinkHeaderPaginationStrategy.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/pagination/LinkHeaderPaginationStrategy.kt index f759da46..a470993c 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/pagination/LinkHeaderPaginationStrategy.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/pagination/LinkHeaderPaginationStrategy.kt @@ -108,16 +108,12 @@ public class LinkHeaderPaginationStrategy * header values) and returns the URL of the first link-value whose `rel` parameter * contains the token `next`, or `null` if no such link-value exists. */ - private fun extractNextUrl(header: String): String? { - for (entry in splitLinkValues(header)) { - val parsed = parseLinkValue(entry) ?: continue - val rels = parsed.second - if (rels.any { it.equals("next", ignoreCase = true) }) { - return parsed.first - } - } - return null - } + private fun extractNextUrl(header: String): String? = + splitLinkValues(header) + .asSequence() + .mapNotNull { parseLinkValue(it) } + .firstOrNull { it.second.any { rel -> rel.equals("next", ignoreCase = true) } } + ?.first /** * Splits an RFC 5988 `Link` header into individual link-values. Commas inside diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/pagination/PageNumberPaginationStrategy.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/pagination/PageNumberPaginationStrategy.kt index 7140aea0..a4b39fc4 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/pagination/PageNumberPaginationStrategy.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/pagination/PageNumberPaginationStrategy.kt @@ -65,15 +65,8 @@ public class PageNumberPaginationStrategy private fun parsePageOrDefault( raw: String?, fallback: Int, - ): Int { - if (raw.isNullOrEmpty()) return fallback - return try { - raw.toInt() - } catch (e: NumberFormatException) { - // Server echoed back a non-numeric "page" param — fall back rather than crash. - @Suppress("UNUSED_VARIABLE") - val ignored = e - fallback - } - } + ): Int = + // Null, empty, or a non-numeric "page" param echoed back by the server all fall back + // rather than crash. + raw?.toIntOrNull() ?: fallback } diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/pagination/RequestRebuilder.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/pagination/RequestRebuilder.kt index 16bd0655..d9cc8847 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/pagination/RequestRebuilder.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/pagination/RequestRebuilder.kt @@ -127,13 +127,11 @@ internal object RequestRebuilder { private fun decodeOrRaw(raw: String): String = try { URLDecoder.decode(raw, UTF_8) - } catch (e: IllegalArgumentException) { + } catch (ignored: IllegalArgumentException) { // Malformed percent-encoding — return raw so equality with caller's name still works // for unencoded ASCII identifiers (the common case for pagination params). - // We intentionally swallow `e` here; pagination should not fail on malformed legacy - // URLs that the transport accepted. - @Suppress("UNUSED_VARIABLE") - val ignored = e + // We intentionally swallow the exception here; pagination should not fail on malformed + // legacy URLs that the transport accepted. raw } diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/pipeline/ResponsePipeline.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/pipeline/ResponsePipeline.kt index 6b253af3..6ebafd0e 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/pipeline/ResponsePipeline.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/pipeline/ResponsePipeline.kt @@ -89,11 +89,9 @@ public class ResponsePipeline outcome: ResponseOutcome, context: DispatchContext, ): ResponseOutcome { - var current = applyResponseSteps(outcome, context) - for (recovery in recoverySteps) { - current = invokeRecovery(recovery, current) + return recoverySteps.fold(applyResponseSteps(outcome, context)) { current, recovery -> + invokeRecovery(recovery, current) } - return current } private fun applyResponseSteps( diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/pipeline/step/ThrowOnHttpErrorStep.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/pipeline/step/ThrowOnHttpErrorStep.kt index 21326999..2c501c85 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/pipeline/step/ThrowOnHttpErrorStep.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/pipeline/step/ThrowOnHttpErrorStep.kt @@ -7,6 +7,7 @@ package org.dexpace.sdk.core.pipeline.step +import org.dexpace.sdk.core.http.common.MediaType import org.dexpace.sdk.core.http.context.DispatchContext import org.dexpace.sdk.core.http.response.Response import org.dexpace.sdk.core.http.response.ResponseBody @@ -105,7 +106,7 @@ public object ThrowOnHttpErrorStep : ResponsePipelineStep { readUpTo(source, cap) } return object : ResponseBody() { - override fun mediaType() = mediaType + override fun mediaType(): MediaType? = mediaType override fun contentLength(): Long = bytes.size.toLong() diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/pipeline/step/retry/RetryStep.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/pipeline/step/retry/RetryStep.kt index 2bae727b..dd262950 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/pipeline/step/retry/RetryStep.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/pipeline/step/retry/RetryStep.kt @@ -180,8 +180,7 @@ public class RetryStep ): ResponseOutcome { var lastError: Throwable = initialError while (true) { - val readyState = prepareNextAttempt(lastError, state) - when (readyState) { + when (val readyState = prepareNextAttempt(lastError, state)) { is AttemptStep.Abort -> return ResponseOutcome.Failure(readyState.error) is AttemptStep.Proceed -> { state.attempt += 1 @@ -240,7 +239,7 @@ public class RetryStep */ private sealed class AttemptStep { /** The deadline is fine, the wait completed; the caller should re-execute. */ - object Proceed : AttemptStep() + data object Proceed : AttemptStep() /** The caller should give up; [error] is the throwable to surface. */ class Abort(val error: Throwable) : AttemptStep() diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/util/Annotations.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/util/Annotations.kt index d96d8378..f30eb266 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/util/Annotations.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/util/Annotations.kt @@ -15,7 +15,7 @@ package org.dexpace.sdk.core.util * `KClass`. Note that this only inspects annotations declared directly on the class — it does not * walk supertypes, interfaces, or meta-annotations. */ -internal inline fun Any.getAnnotation(): T? = this::class.annotations.firstOrNull { it is T } as? T +internal inline fun Any.getAnnotation(): T? = this::class.annotations.filterIsInstance().firstOrNull() /** * Reflectively reports whether `this` instance's runtime class declares an annotation of type [T]. diff --git a/sdk-io-okio3/src/main/kotlin/org/dexpace/sdk/io/internal/ForeignSinkAdapter.kt b/sdk-io-okio3/src/main/kotlin/org/dexpace/sdk/io/internal/ForeignSinkAdapter.kt index 4560bc1e..e374d94b 100644 --- a/sdk-io-okio3/src/main/kotlin/org/dexpace/sdk/io/internal/ForeignSinkAdapter.kt +++ b/sdk-io-okio3/src/main/kotlin/org/dexpace/sdk/io/internal/ForeignSinkAdapter.kt @@ -22,7 +22,6 @@ import org.dexpace.sdk.core.io.Sink * wrapper field is read/written without synchronization. */ internal class ForeignSinkAdapter(private val delegate: Sink) : okio.Sink { - private val timeout = okio.Timeout.NONE private var cachedBuffer: okio.Buffer? = null private var cachedWrapper: OkioBuffer? = null @@ -46,7 +45,7 @@ internal class ForeignSinkAdapter(private val delegate: Sink) : okio.Sink { delegate.flush() } - override fun timeout(): okio.Timeout = timeout + override fun timeout(): okio.Timeout = okio.Timeout.NONE override fun close() { delegate.close() diff --git a/sdk-io-okio3/src/main/kotlin/org/dexpace/sdk/io/internal/ForeignSourceAdapter.kt b/sdk-io-okio3/src/main/kotlin/org/dexpace/sdk/io/internal/ForeignSourceAdapter.kt index 15d3f1d5..98eed474 100644 --- a/sdk-io-okio3/src/main/kotlin/org/dexpace/sdk/io/internal/ForeignSourceAdapter.kt +++ b/sdk-io-okio3/src/main/kotlin/org/dexpace/sdk/io/internal/ForeignSourceAdapter.kt @@ -23,7 +23,6 @@ import org.dexpace.sdk.core.io.Source * wrapper field is read/written without synchronization. */ internal class ForeignSourceAdapter(private val delegate: Source) : okio.Source { - private val timeout = okio.Timeout.NONE private var cachedBuffer: okio.Buffer? = null private var cachedWrapper: OkioBuffer? = null @@ -43,7 +42,7 @@ internal class ForeignSourceAdapter(private val delegate: Source) : okio.Source return delegate.read(wrapper, byteCount) } - override fun timeout(): okio.Timeout = timeout + override fun timeout(): okio.Timeout = okio.Timeout.NONE override fun close() { delegate.close() diff --git a/sdk-io-okio3/src/main/kotlin/org/dexpace/sdk/io/internal/WriteAllInto.kt b/sdk-io-okio3/src/main/kotlin/org/dexpace/sdk/io/internal/WriteAllInto.kt index 15f72e92..0966a998 100644 --- a/sdk-io-okio3/src/main/kotlin/org/dexpace/sdk/io/internal/WriteAllInto.kt +++ b/sdk-io-okio3/src/main/kotlin/org/dexpace/sdk/io/internal/WriteAllInto.kt @@ -37,9 +37,9 @@ internal fun writeAllInto( var total = 0L while (true) { val read = source.read(tmp, SEGMENT_SIZE) - when { - read == -1L -> break // EOF — normal termination - read == 0L -> throw IOException( + when (read) { + -1L -> break // EOF — normal termination + 0L -> throw IOException( "Source returned 0 for byteCount=$SEGMENT_SIZE which violates the Source.read contract", ) else -> { diff --git a/sdk-serde-jackson/src/main/kotlin/org/dexpace/sdk/serde/jackson/TristateModule.kt b/sdk-serde-jackson/src/main/kotlin/org/dexpace/sdk/serde/jackson/TristateModule.kt index 77ab908e..7a08a587 100644 --- a/sdk-serde-jackson/src/main/kotlin/org/dexpace/sdk/serde/jackson/TristateModule.kt +++ b/sdk-serde-jackson/src/main/kotlin/org/dexpace/sdk/serde/jackson/TristateModule.kt @@ -225,8 +225,7 @@ internal class TristateSerializerModifier internal constructor() : BeanSerialize beanDesc: BeanDescription, beanProperties: MutableList, ): MutableList { - for (i in beanProperties.indices) { - val writer = beanProperties[i] + beanProperties.forEachIndexed { i, writer -> if (Tristate::class.java.isAssignableFrom(writer.type.rawClass)) { beanProperties[i] = TristatePropertyWriter(writer) } diff --git a/sdk-transport-jdkhttp/src/main/kotlin/org/dexpace/sdk/transport/jdkhttp/JdkHttpTransport.kt b/sdk-transport-jdkhttp/src/main/kotlin/org/dexpace/sdk/transport/jdkhttp/JdkHttpTransport.kt index 65f90dd1..283ff902 100644 --- a/sdk-transport-jdkhttp/src/main/kotlin/org/dexpace/sdk/transport/jdkhttp/JdkHttpTransport.kt +++ b/sdk-transport-jdkhttp/src/main/kotlin/org/dexpace/sdk/transport/jdkhttp/JdkHttpTransport.kt @@ -347,10 +347,9 @@ public class JdkHttpTransport private constructor( }, ) clientBuilder.version( - if (httpVersion == HttpVersion.HTTP_2) { - java.net.http.HttpClient.Version.HTTP_2 - } else { - java.net.http.HttpClient.Version.HTTP_1_1 + when (httpVersion) { + HttpVersion.HTTP_2 -> java.net.http.HttpClient.Version.HTTP_2 + HttpVersion.HTTP_1_1 -> java.net.http.HttpClient.Version.HTTP_1_1 }, ) proxy?.let { applyProxy(clientBuilder, it) } diff --git a/sdk-transport-jdkhttp/src/main/kotlin/org/dexpace/sdk/transport/jdkhttp/internal/RestrictedHeaders.kt b/sdk-transport-jdkhttp/src/main/kotlin/org/dexpace/sdk/transport/jdkhttp/internal/RestrictedHeaders.kt index a93ed52c..e05c98b9 100644 --- a/sdk-transport-jdkhttp/src/main/kotlin/org/dexpace/sdk/transport/jdkhttp/internal/RestrictedHeaders.kt +++ b/sdk-transport-jdkhttp/src/main/kotlin/org/dexpace/sdk/transport/jdkhttp/internal/RestrictedHeaders.kt @@ -43,12 +43,12 @@ import java.util.Locale internal object RestrictedHeaders { private val NAMES: Set = setOf( - "connection".lowercase(Locale.US), - "content-length".lowercase(Locale.US), - "expect".lowercase(Locale.US), - "host".lowercase(Locale.US), - "transfer-encoding".lowercase(Locale.US), - "upgrade".lowercase(Locale.US), + "connection", + "content-length", + "expect", + "host", + "transfer-encoding", + "upgrade", ) /** Returns `true` when [name] (case-insensitive) is in the drop list. */ diff --git a/sdk-transport-okhttp/src/main/kotlin/org/dexpace/sdk/transport/okhttp/internal/RequestAdapter.kt b/sdk-transport-okhttp/src/main/kotlin/org/dexpace/sdk/transport/okhttp/internal/RequestAdapter.kt index 88af552b..f83d7059 100644 --- a/sdk-transport-okhttp/src/main/kotlin/org/dexpace/sdk/transport/okhttp/internal/RequestAdapter.kt +++ b/sdk-transport-okhttp/src/main/kotlin/org/dexpace/sdk/transport/okhttp/internal/RequestAdapter.kt @@ -58,9 +58,10 @@ internal class RequestAdapter( } } val methodToken = request.method.method + val body = request.body val okhttpBody = when { - request.body != null -> toOkHttpBody(request.body!!) + body != null -> toOkHttpBody(body) // OkHttp rejects a null body for the methods it treats as requiring one. The // SDK allows a body-less request for any method, so substitute an empty body // here rather than letting Request.Builder.method throw. diff --git a/sdk-transport-okhttp/src/main/kotlin/org/dexpace/sdk/transport/okhttp/internal/RestrictedHeaders.kt b/sdk-transport-okhttp/src/main/kotlin/org/dexpace/sdk/transport/okhttp/internal/RestrictedHeaders.kt index 06790450..e6780cac 100644 --- a/sdk-transport-okhttp/src/main/kotlin/org/dexpace/sdk/transport/okhttp/internal/RestrictedHeaders.kt +++ b/sdk-transport-okhttp/src/main/kotlin/org/dexpace/sdk/transport/okhttp/internal/RestrictedHeaders.kt @@ -26,9 +26,9 @@ import java.util.Locale internal object RestrictedHeaders { private val NAMES: Set = setOf( - "content-length".lowercase(Locale.US), - "host".lowercase(Locale.US), - "transfer-encoding".lowercase(Locale.US), + "content-length", + "host", + "transfer-encoding", ) /** Returns `true` when [name] (case-insensitive) is in the drop list. */