Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.1.0-alpha.44"
".": "0.1.0-alpha.45"
}
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

<!-- x-release-please-start-version -->

[![Maven Central](https://img.shields.io/maven-central/v/org.onebusaway/onebusaway-sdk-java)](https://central.sonatype.com/artifact/org.onebusaway/onebusaway-sdk-java/0.1.0-alpha.44)
[![javadoc](https://javadoc.io/badge2/org.onebusaway/onebusaway-sdk-java/0.1.0-alpha.44/javadoc.svg)](https://javadoc.io/doc/org.onebusaway/onebusaway-sdk-java/0.1.0-alpha.44)
[![Maven Central](https://img.shields.io/maven-central/v/org.onebusaway/onebusaway-sdk-java)](https://central.sonatype.com/artifact/org.onebusaway/onebusaway-sdk-java/0.1.0-alpha.45)
[![javadoc](https://javadoc.io/badge2/org.onebusaway/onebusaway-sdk-java/0.1.0-alpha.45/javadoc.svg)](https://javadoc.io/doc/org.onebusaway/onebusaway-sdk-java/0.1.0-alpha.45)

<!-- x-release-please-end -->

Expand All @@ -15,7 +15,7 @@ It is generated with [Stainless](https://www.stainless.com/).

<!-- x-release-please-start-version -->

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).

<!-- x-release-please-end -->

Expand All @@ -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
Expand All @@ -35,7 +35,7 @@ implementation("org.onebusaway:onebusaway-sdk-java:0.1.0-alpha.44")
<dependency>
<groupId>org.onebusaway</groupId>
<artifactId>onebusaway-sdk-java</artifactId>
<version>0.1.0-alpha.44</version>
<version>0.1.0-alpha.45</version>
</dependency>
```

Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
}
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<ExecutorService>) =
dispatcherExecutorService(dispatcherExecutorService.getOrNull())

fun proxy(proxy: Proxy?) = apply { this.proxy = proxy }

/** Alias for calling [Builder.proxy] with `proxy.orElse(null)`. */
Expand Down Expand Up @@ -296,6 +317,7 @@ class OnebusawaySdkOkHttpClient private constructor() {
OkHttpClient.builder()
.timeout(clientOptions.timeout())
.proxy(proxy)
.dispatcherExecutorService(dispatcherExecutorService)
.sslSocketFactory(sslSocketFactory)
.trustManager(trustManager)
.hostnameVerifier(hostnameVerifier)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<ExecutorService>) =
dispatcherExecutorService(dispatcherExecutorService.getOrNull())

fun proxy(proxy: Proxy?) = apply { this.proxy = proxy }

/** Alias for calling [Builder.proxy] with `proxy.orElse(null)`. */
Expand Down Expand Up @@ -296,6 +317,7 @@ class OnebusawaySdkOkHttpClientAsync private constructor() {
OkHttpClient.builder()
.timeout(clientOptions.timeout())
.proxy(proxy)
.dispatcherExecutorService(dispatcherExecutorService)
.sslSocketFactory(sslSocketFactory)
.trustManager(trustManager)
.hostnameVerifier(hostnameVerifier)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.onebusaway.core.http

import java.net.URLEncoder
import org.onebusaway.core.checkRequired
import org.onebusaway.core.toImmutable

Expand All @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}