diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index b10d67a..c5407a0 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.1.0-alpha.44"
+ ".": "0.1.0-alpha.45"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2fc649f..984dbd0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# Changelog
+## 0.1.0-alpha.45 (2026-01-09)
+
+Full Changelog: [v0.1.0-alpha.44...v0.1.0-alpha.45](https://github.com/OneBusAway/java-sdk/compare/v0.1.0-alpha.44...v0.1.0-alpha.45)
+
+### Features
+
+* **client:** add `HttpRequest#url()` method ([d7e318b](https://github.com/OneBusAway/java-sdk/commit/d7e318b548f644a4dc9e55f5ebc6604282fe57a3))
+* **client:** allow configuring dispatcher executor service ([4ff1629](https://github.com/OneBusAway/java-sdk/commit/4ff16297c9acc59aba99ff7cb1fe278e73c7d38b))
+
## 0.1.0-alpha.44 (2025-12-03)
Full Changelog: [v0.1.0-alpha.43...v0.1.0-alpha.44](https://github.com/OneBusAway/java-sdk/compare/v0.1.0-alpha.43...v0.1.0-alpha.44)
diff --git a/LICENSE b/LICENSE
index 443d70c..26680ba 100644
--- a/LICENSE
+++ b/LICENSE
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
- Copyright 2025 Onebusaway SDK
+ Copyright 2026 Onebusaway SDK
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/README.md b/README.md
index 477ee22..bf0bb88 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,8 @@
-[](https://central.sonatype.com/artifact/org.onebusaway/onebusaway-sdk-java/0.1.0-alpha.44)
-[](https://javadoc.io/doc/org.onebusaway/onebusaway-sdk-java/0.1.0-alpha.44)
+[](https://central.sonatype.com/artifact/org.onebusaway/onebusaway-sdk-java/0.1.0-alpha.45)
+[](https://javadoc.io/doc/org.onebusaway/onebusaway-sdk-java/0.1.0-alpha.45)
@@ -15,7 +15,7 @@ It is generated with [Stainless](https://www.stainless.com/).
-The REST API documentation can be found on [developer.onebusaway.org](https://developer.onebusaway.org). Javadocs are available on [javadoc.io](https://javadoc.io/doc/org.onebusaway/onebusaway-sdk-java/0.1.0-alpha.44).
+The REST API documentation can be found on [developer.onebusaway.org](https://developer.onebusaway.org). Javadocs are available on [javadoc.io](https://javadoc.io/doc/org.onebusaway/onebusaway-sdk-java/0.1.0-alpha.45).
@@ -26,7 +26,7 @@ The REST API documentation can be found on [developer.onebusaway.org](https://de
### Gradle
```kotlin
-implementation("org.onebusaway:onebusaway-sdk-java:0.1.0-alpha.44")
+implementation("org.onebusaway:onebusaway-sdk-java:0.1.0-alpha.45")
```
### Maven
@@ -35,7 +35,7 @@ implementation("org.onebusaway:onebusaway-sdk-java:0.1.0-alpha.44")
org.onebusaway
onebusaway-sdk-java
- 0.1.0-alpha.44
+ 0.1.0-alpha.45
```
diff --git a/build.gradle.kts b/build.gradle.kts
index da4f16b..a0cf580 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -8,7 +8,7 @@ repositories {
allprojects {
group = "org.onebusaway"
- version = "0.1.0-alpha.44" // x-release-please-version
+ version = "0.1.0-alpha.45" // x-release-please-version
}
subprojects {
diff --git a/onebusaway-sdk-java-client-okhttp/src/main/kotlin/org/onebusaway/client/okhttp/OkHttpClient.kt b/onebusaway-sdk-java-client-okhttp/src/main/kotlin/org/onebusaway/client/okhttp/OkHttpClient.kt
index f1ac2e5..81ec160 100644
--- a/onebusaway-sdk-java-client-okhttp/src/main/kotlin/org/onebusaway/client/okhttp/OkHttpClient.kt
+++ b/onebusaway-sdk-java-client-okhttp/src/main/kotlin/org/onebusaway/client/okhttp/OkHttpClient.kt
@@ -6,11 +6,13 @@ import java.net.Proxy
import java.time.Duration
import java.util.concurrent.CancellationException
import java.util.concurrent.CompletableFuture
+import java.util.concurrent.ExecutorService
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.X509TrustManager
import okhttp3.Call
import okhttp3.Callback
+import okhttp3.Dispatcher
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.MediaType
@@ -200,6 +202,7 @@ private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClien
private var timeout: Timeout = Timeout.default()
private var proxy: Proxy? = null
+ private var dispatcherExecutorService: ExecutorService? = null
private var sslSocketFactory: SSLSocketFactory? = null
private var trustManager: X509TrustManager? = null
private var hostnameVerifier: HostnameVerifier? = null
@@ -210,6 +213,10 @@ private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClien
fun proxy(proxy: Proxy?) = apply { this.proxy = proxy }
+ fun dispatcherExecutorService(dispatcherExecutorService: ExecutorService?) = apply {
+ this.dispatcherExecutorService = dispatcherExecutorService
+ }
+
fun sslSocketFactory(sslSocketFactory: SSLSocketFactory?) = apply {
this.sslSocketFactory = sslSocketFactory
}
@@ -231,6 +238,8 @@ private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClien
.callTimeout(timeout.request())
.proxy(proxy)
.apply {
+ dispatcherExecutorService?.let { dispatcher(Dispatcher(it)) }
+
val sslSocketFactory = sslSocketFactory
val trustManager = trustManager
if (sslSocketFactory != null && trustManager != null) {
diff --git a/onebusaway-sdk-java-client-okhttp/src/main/kotlin/org/onebusaway/client/okhttp/OnebusawaySdkOkHttpClient.kt b/onebusaway-sdk-java-client-okhttp/src/main/kotlin/org/onebusaway/client/okhttp/OnebusawaySdkOkHttpClient.kt
index fea0327..1399e4c 100644
--- a/onebusaway-sdk-java-client-okhttp/src/main/kotlin/org/onebusaway/client/okhttp/OnebusawaySdkOkHttpClient.kt
+++ b/onebusaway-sdk-java-client-okhttp/src/main/kotlin/org/onebusaway/client/okhttp/OnebusawaySdkOkHttpClient.kt
@@ -7,6 +7,7 @@ import java.net.Proxy
import java.time.Clock
import java.time.Duration
import java.util.Optional
+import java.util.concurrent.ExecutorService
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.X509TrustManager
@@ -44,11 +45,31 @@ class OnebusawaySdkOkHttpClient private constructor() {
class Builder internal constructor() {
private var clientOptions: ClientOptions.Builder = ClientOptions.builder()
+ private var dispatcherExecutorService: ExecutorService? = null
private var proxy: Proxy? = null
private var sslSocketFactory: SSLSocketFactory? = null
private var trustManager: X509TrustManager? = null
private var hostnameVerifier: HostnameVerifier? = null
+ /**
+ * The executor service to use for running HTTP requests.
+ *
+ * Defaults to OkHttp's
+ * [default executor service](https://github.com/square/okhttp/blob/ace792f443b2ffb17974f5c0d1cecdf589309f26/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Dispatcher.kt#L98-L104).
+ *
+ * This class takes ownership of the executor service and shuts it down when closed.
+ */
+ fun dispatcherExecutorService(dispatcherExecutorService: ExecutorService?) = apply {
+ this.dispatcherExecutorService = dispatcherExecutorService
+ }
+
+ /**
+ * Alias for calling [Builder.dispatcherExecutorService] with
+ * `dispatcherExecutorService.orElse(null)`.
+ */
+ fun dispatcherExecutorService(dispatcherExecutorService: Optional) =
+ dispatcherExecutorService(dispatcherExecutorService.getOrNull())
+
fun proxy(proxy: Proxy?) = apply { this.proxy = proxy }
/** Alias for calling [Builder.proxy] with `proxy.orElse(null)`. */
@@ -296,6 +317,7 @@ class OnebusawaySdkOkHttpClient private constructor() {
OkHttpClient.builder()
.timeout(clientOptions.timeout())
.proxy(proxy)
+ .dispatcherExecutorService(dispatcherExecutorService)
.sslSocketFactory(sslSocketFactory)
.trustManager(trustManager)
.hostnameVerifier(hostnameVerifier)
diff --git a/onebusaway-sdk-java-client-okhttp/src/main/kotlin/org/onebusaway/client/okhttp/OnebusawaySdkOkHttpClientAsync.kt b/onebusaway-sdk-java-client-okhttp/src/main/kotlin/org/onebusaway/client/okhttp/OnebusawaySdkOkHttpClientAsync.kt
index cda8a74..9a4f625 100644
--- a/onebusaway-sdk-java-client-okhttp/src/main/kotlin/org/onebusaway/client/okhttp/OnebusawaySdkOkHttpClientAsync.kt
+++ b/onebusaway-sdk-java-client-okhttp/src/main/kotlin/org/onebusaway/client/okhttp/OnebusawaySdkOkHttpClientAsync.kt
@@ -7,6 +7,7 @@ import java.net.Proxy
import java.time.Clock
import java.time.Duration
import java.util.Optional
+import java.util.concurrent.ExecutorService
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.X509TrustManager
@@ -44,11 +45,31 @@ class OnebusawaySdkOkHttpClientAsync private constructor() {
class Builder internal constructor() {
private var clientOptions: ClientOptions.Builder = ClientOptions.builder()
+ private var dispatcherExecutorService: ExecutorService? = null
private var proxy: Proxy? = null
private var sslSocketFactory: SSLSocketFactory? = null
private var trustManager: X509TrustManager? = null
private var hostnameVerifier: HostnameVerifier? = null
+ /**
+ * The executor service to use for running HTTP requests.
+ *
+ * Defaults to OkHttp's
+ * [default executor service](https://github.com/square/okhttp/blob/ace792f443b2ffb17974f5c0d1cecdf589309f26/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Dispatcher.kt#L98-L104).
+ *
+ * This class takes ownership of the executor service and shuts it down when closed.
+ */
+ fun dispatcherExecutorService(dispatcherExecutorService: ExecutorService?) = apply {
+ this.dispatcherExecutorService = dispatcherExecutorService
+ }
+
+ /**
+ * Alias for calling [Builder.dispatcherExecutorService] with
+ * `dispatcherExecutorService.orElse(null)`.
+ */
+ fun dispatcherExecutorService(dispatcherExecutorService: Optional) =
+ dispatcherExecutorService(dispatcherExecutorService.getOrNull())
+
fun proxy(proxy: Proxy?) = apply { this.proxy = proxy }
/** Alias for calling [Builder.proxy] with `proxy.orElse(null)`. */
@@ -296,6 +317,7 @@ class OnebusawaySdkOkHttpClientAsync private constructor() {
OkHttpClient.builder()
.timeout(clientOptions.timeout())
.proxy(proxy)
+ .dispatcherExecutorService(dispatcherExecutorService)
.sslSocketFactory(sslSocketFactory)
.trustManager(trustManager)
.hostnameVerifier(hostnameVerifier)
diff --git a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/HttpRequest.kt b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/HttpRequest.kt
index 81a3ed9..27421c7 100644
--- a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/HttpRequest.kt
+++ b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/HttpRequest.kt
@@ -1,5 +1,6 @@
package org.onebusaway.core.http
+import java.net.URLEncoder
import org.onebusaway.core.checkRequired
import org.onebusaway.core.toImmutable
@@ -13,6 +14,35 @@ private constructor(
@get:JvmName("body") val body: HttpRequestBody?,
) {
+ fun url(): String = buildString {
+ append(baseUrl)
+
+ pathSegments.forEach { segment ->
+ if (!endsWith("/")) {
+ append("/")
+ }
+ append(URLEncoder.encode(segment, "UTF-8"))
+ }
+
+ if (queryParams.isEmpty()) {
+ return@buildString
+ }
+
+ append("?")
+ var isFirst = true
+ queryParams.keys().forEach { key ->
+ queryParams.values(key).forEach { value ->
+ if (!isFirst) {
+ append("&")
+ }
+ append(URLEncoder.encode(key, "UTF-8"))
+ append("=")
+ append(URLEncoder.encode(value, "UTF-8"))
+ isFirst = false
+ }
+ }
+ }
+
fun toBuilder(): Builder = Builder().from(this)
override fun toString(): String =
diff --git a/onebusaway-sdk-java-core/src/test/kotlin/org/onebusaway/core/http/HttpRequestTest.kt b/onebusaway-sdk-java-core/src/test/kotlin/org/onebusaway/core/http/HttpRequestTest.kt
new file mode 100644
index 0000000..b848b8d
--- /dev/null
+++ b/onebusaway-sdk-java-core/src/test/kotlin/org/onebusaway/core/http/HttpRequestTest.kt
@@ -0,0 +1,110 @@
+package org.onebusaway.core.http
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.EnumSource
+
+internal class HttpRequestTest {
+
+ enum class UrlTestCase(val request: HttpRequest, val expectedUrl: String) {
+ BASE_URL_ONLY(
+ HttpRequest.builder().method(HttpMethod.GET).baseUrl("https://api.example.com").build(),
+ expectedUrl = "https://api.example.com",
+ ),
+ BASE_URL_WITH_TRAILING_SLASH(
+ HttpRequest.builder()
+ .method(HttpMethod.GET)
+ .baseUrl("https://api.example.com/")
+ .build(),
+ expectedUrl = "https://api.example.com/",
+ ),
+ SINGLE_PATH_SEGMENT(
+ HttpRequest.builder()
+ .method(HttpMethod.GET)
+ .baseUrl("https://api.example.com")
+ .addPathSegment("users")
+ .build(),
+ expectedUrl = "https://api.example.com/users",
+ ),
+ MULTIPLE_PATH_SEGMENTS(
+ HttpRequest.builder()
+ .method(HttpMethod.GET)
+ .baseUrl("https://api.example.com")
+ .addPathSegments("users", "123", "profile")
+ .build(),
+ expectedUrl = "https://api.example.com/users/123/profile",
+ ),
+ PATH_SEGMENT_WITH_SPECIAL_CHARS(
+ HttpRequest.builder()
+ .method(HttpMethod.GET)
+ .baseUrl("https://api.example.com")
+ .addPathSegment("user name")
+ .build(),
+ expectedUrl = "https://api.example.com/user+name",
+ ),
+ SINGLE_QUERY_PARAM(
+ HttpRequest.builder()
+ .method(HttpMethod.GET)
+ .baseUrl("https://api.example.com")
+ .addPathSegment("users")
+ .putQueryParam("limit", "10")
+ .build(),
+ expectedUrl = "https://api.example.com/users?limit=10",
+ ),
+ MULTIPLE_QUERY_PARAMS(
+ HttpRequest.builder()
+ .method(HttpMethod.GET)
+ .baseUrl("https://api.example.com")
+ .addPathSegment("users")
+ .putQueryParam("limit", "10")
+ .putQueryParam("offset", "20")
+ .build(),
+ expectedUrl = "https://api.example.com/users?limit=10&offset=20",
+ ),
+ QUERY_PARAM_WITH_SPECIAL_CHARS(
+ HttpRequest.builder()
+ .method(HttpMethod.GET)
+ .baseUrl("https://api.example.com")
+ .addPathSegment("search")
+ .putQueryParam("q", "hello world")
+ .build(),
+ expectedUrl = "https://api.example.com/search?q=hello+world",
+ ),
+ MULTIPLE_VALUES_SAME_PARAM(
+ HttpRequest.builder()
+ .method(HttpMethod.GET)
+ .baseUrl("https://api.example.com")
+ .addPathSegment("users")
+ .putQueryParams("tags", listOf("admin", "user"))
+ .build(),
+ expectedUrl = "https://api.example.com/users?tags=admin&tags=user",
+ ),
+ BASE_URL_WITH_TRAILING_SLASH_AND_PATH(
+ HttpRequest.builder()
+ .method(HttpMethod.GET)
+ .baseUrl("https://api.example.com/")
+ .addPathSegment("users")
+ .build(),
+ expectedUrl = "https://api.example.com/users",
+ ),
+ COMPLEX_URL(
+ HttpRequest.builder()
+ .method(HttpMethod.POST)
+ .baseUrl("https://api.example.com")
+ .addPathSegments("v1", "users", "123")
+ .putQueryParams("include", listOf("profile", "settings"))
+ .putQueryParam("format", "json")
+ .build(),
+ expectedUrl =
+ "https://api.example.com/v1/users/123?include=profile&include=settings&format=json",
+ ),
+ }
+
+ @ParameterizedTest
+ @EnumSource
+ fun url(testCase: UrlTestCase) {
+ val actualUrl = testCase.request.url()
+
+ assertThat(actualUrl).isEqualTo(testCase.expectedUrl)
+ }
+}