diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 71594a4..ef24b0c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,43 +1,40 @@
name: CI
on:
push:
- branches:
- - main
- pull_request:
- branches:
- - main
- - next
+ branches-ignore:
+ - 'generated'
+ - 'codegen/**'
+ - 'integrated/**'
+ - 'stl-preview-head/**'
+ - 'stl-preview-base/**'
jobs:
lint:
+ timeout-minutes: 10
name: lint
- runs-on: ubuntu-latest
-
+ runs-on: ${{ github.repository == 'stainless-sdks/open-transit-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@v4
- - name: Validate Gradle wrapper
- uses: gradle/actions/wrapper-validation@v3
-
- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: |
8
- 17
+ 21
cache: gradle
- name: Set up Gradle
- uses: gradle/gradle-build-action@v2
+ uses: gradle/actions/setup-gradle@v4
- name: Run lints
run: ./scripts/lint
test:
+ timeout-minutes: 10
name: test
- runs-on: ubuntu-latest
-
+ runs-on: ${{ github.repository == 'stainless-sdks/open-transit-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@v4
@@ -47,7 +44,7 @@ jobs:
distribution: temurin
java-version: |
8
- 17
+ 21
cache: gradle
- name: Set up Gradle
@@ -55,4 +52,3 @@ jobs:
- name: Run tests
run: ./scripts/test
-
diff --git a/.github/workflows/publish-sonatype.yml b/.github/workflows/publish-sonatype.yml
index 1247fe9..04592b7 100644
--- a/.github/workflows/publish-sonatype.yml
+++ b/.github/workflows/publish-sonatype.yml
@@ -29,11 +29,13 @@ jobs:
uses: gradle/gradle-build-action@v2
- name: Publish to Sonatype
- run: |
- ./gradlew publishAndReleaseToMavenCentral --stacktrace -PmavenCentralUsername="$SONATYPE_USERNAME" -PmavenCentralPassword="$SONATYPE_PASSWORD"
+ run: |-
+ export -- GPG_SIGNING_KEY_ID
+ printenv -- GPG_SIGNING_KEY | gpg --batch --passphrase-fd 3 --import 3<<< "$GPG_SIGNING_PASSWORD"
+ GPG_SIGNING_KEY_ID="$(gpg --with-colons --list-keys | awk -F : -- '/^pub:/ { getline; print "0x" substr($10, length($10) - 7) }')"
+ ./gradlew publishAndReleaseToMavenCentral --stacktrace -PmavenCentralUsername="$SONATYPE_USERNAME" -PmavenCentralPassword="$SONATYPE_PASSWORD" --no-configuration-cache
env:
SONATYPE_USERNAME: ${{ secrets.ONEBUSAWAY_SDK_SONATYPE_USERNAME || secrets.SONATYPE_USERNAME }}
SONATYPE_PASSWORD: ${{ secrets.ONEBUSAWAY_SDK_SONATYPE_PASSWORD || secrets.SONATYPE_PASSWORD }}
- GPG_SIGNING_KEY_ID: ${{ secrets.ONEBUSAWAY_SDK_SONATYPE_GPG_SIGNING_KEY_ID || secrets.GPG_SIGNING_KEY_ID }}
GPG_SIGNING_KEY: ${{ secrets.ONEBUSAWAY_SDK_SONATYPE_GPG_SIGNING_KEY || secrets.GPG_SIGNING_KEY }}
GPG_SIGNING_PASSWORD: ${{ secrets.ONEBUSAWAY_SDK_SONATYPE_GPG_SIGNING_PASSWORD || secrets.GPG_SIGNING_PASSWORD }}
\ No newline at end of file
diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml
index f328321..fac0542 100644
--- a/.github/workflows/release-doctor.yml
+++ b/.github/workflows/release-doctor.yml
@@ -20,6 +20,5 @@ jobs:
env:
SONATYPE_USERNAME: ${{ secrets.ONEBUSAWAY_SDK_SONATYPE_USERNAME || secrets.SONATYPE_USERNAME }}
SONATYPE_PASSWORD: ${{ secrets.ONEBUSAWAY_SDK_SONATYPE_PASSWORD || secrets.SONATYPE_PASSWORD }}
- GPG_SIGNING_KEY_ID: ${{ secrets.ONEBUSAWAY_SDK_SONATYPE_GPG_SIGNING_KEY_ID || secrets.GPG_SIGNING_KEY_ID }}
GPG_SIGNING_KEY: ${{ secrets.ONEBUSAWAY_SDK_SONATYPE_GPG_SIGNING_KEY || secrets.GPG_SIGNING_KEY }}
GPG_SIGNING_PASSWORD: ${{ secrets.ONEBUSAWAY_SDK_SONATYPE_GPG_SIGNING_PASSWORD || secrets.GPG_SIGNING_PASSWORD }}
diff --git a/.gitignore b/.gitignore
index 39c31e3..4e81838 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
.prism.log
.gradle
.idea
+.kotlin
build
codegen.log
kls_database.db
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 1c0bb88..380b6f9 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.1.0-alpha.23"
+ ".": "0.1.0-alpha.24"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index d4b713b..ad9cf70 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,2 +1,4 @@
configured_endpoints: 29
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/open-transit%2Fopen-transit-6f08502508c8ad25235971add3124a1cde4f1c3ec705d5df455d750e0adcb90b.yml
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/open-transit%2Fopen-transit-4fcbe9547537b22a2d68329e1d94e0c1a6f81b5af734ca213f7b95eef5da7adb.yml
+openapi_spec_hash: 417ea17b08e186b15b2986372592185e
+config_hash: 3871f5d21bb38ddd334ec04721dea64d
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6abeafe..ae94241 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,26 @@
# Changelog
+## 0.1.0-alpha.24 (2025-07-07)
+
+Full Changelog: [v0.1.0-alpha.23...v0.1.0-alpha.24](https://github.com/OneBusAway/java-sdk/compare/v0.1.0-alpha.23...v0.1.0-alpha.24)
+
+### Features
+
+* **api:** api update ([a9266bd](https://github.com/OneBusAway/java-sdk/commit/a9266bd3053952157431e4aee9b36aae6b637164))
+* **api:** manual updates ([#74](https://github.com/OneBusAway/java-sdk/issues/74)) ([ddda17b](https://github.com/OneBusAway/java-sdk/commit/ddda17b7186ef701720740c946250b5fa902a4cc))
+
+
+### Chores
+
+* **internal:** codegen related update ([8f6f893](https://github.com/OneBusAway/java-sdk/commit/8f6f893129b9b061d933432fc8ecc36be5e5ad7f))
+* **internal:** codegen related update ([#71](https://github.com/OneBusAway/java-sdk/issues/71)) ([c5e90c0](https://github.com/OneBusAway/java-sdk/commit/c5e90c0cda29a9131fd81e60d22711794a2f0a71))
+* **internal:** codegen related update ([#73](https://github.com/OneBusAway/java-sdk/issues/73)) ([a45c6b0](https://github.com/OneBusAway/java-sdk/commit/a45c6b0bc2f45328c1365db12b9ed2fca0d85ce6))
+* **internal:** codegen related update ([#75](https://github.com/OneBusAway/java-sdk/issues/75)) ([a433a99](https://github.com/OneBusAway/java-sdk/commit/a433a99937fdc1729f36a4e7d16a8d90137758e8))
+* **internal:** codegen related update ([#76](https://github.com/OneBusAway/java-sdk/issues/76)) ([a8f595e](https://github.com/OneBusAway/java-sdk/commit/a8f595e2ee1038f8161ab784fd189d074f375d07))
+* **internal:** codegen related update ([#77](https://github.com/OneBusAway/java-sdk/issues/77)) ([14ea7b7](https://github.com/OneBusAway/java-sdk/commit/14ea7b7438268138410dca70f28807ec06f5c5b0))
+* **internal:** codegen related update ([#78](https://github.com/OneBusAway/java-sdk/issues/78)) ([51a4c8c](https://github.com/OneBusAway/java-sdk/commit/51a4c8cab3782706a773525083c4b17da4543064))
+* **internal:** update example values ([#72](https://github.com/OneBusAway/java-sdk/issues/72)) ([9f7aadf](https://github.com/OneBusAway/java-sdk/commit/9f7aadf0b9229f441f1dbfed4b64035f195c3b1e))
+
## 0.1.0-alpha.23 (2024-11-29)
Full Changelog: [v0.1.0-alpha.22...v0.1.0-alpha.23](https://github.com/OneBusAway/java-sdk/compare/v0.1.0-alpha.22...v0.1.0-alpha.23)
diff --git a/LICENSE b/LICENSE
index 9e6f6ec..443d70c 100644
--- a/LICENSE
+++ b/LICENSE
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
- Copyright 2024 Onebusaway SDK
+ Copyright 2025 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 7dd1dc3..a9ef60f 100644
--- a/README.md
+++ b/README.md
@@ -2,49 +2,76 @@
-[](https://central.sonatype.com/artifact/org.onebusaway/onebusaway-sdk-java/0.1.0-alpha.23)
+[](https://central.sonatype.com/artifact/org.onebusaway/onebusaway-sdk-java/0.1.0-alpha.24)
+[](https://javadoc.io/doc/org.onebusaway/onebusaway-sdk-java/0.1.0-alpha.24)
-The Onebusaway SDK Java SDK provides convenient access to the Onebusaway SDK REST API from applications written in Java. It includes helper classes with helpful types and documentation for every request and response property.
+The Onebusaway SDK Java SDK provides convenient access to the [Onebusaway SDK REST API](https://developer.onebusaway.org) from applications written in Java.
The Onebusaway SDK Java SDK is similar to the Onebusaway SDK Kotlin SDK but with minor differences that make it more ergonomic for use in Java, such as `Optional` instead of nullable values, `Stream` instead of `Sequence`, and `CompletableFuture` instead of suspend functions.
-It is generated with [Stainless](https://www.stainlessapi.com/).
+It is generated with [Stainless](https://www.stainless.com/).
-## Documentation
-
-The REST API documentation can be found on [developer.onebusaway.org](https://developer.onebusaway.org).
-
----
+
-## Getting started
+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.24).
-### Install dependencies
+
-#### Gradle
+## Installation
+### Gradle
+
```kotlin
-implementation("org.onebusaway:onebusaway-sdk-java:0.1.0-alpha.23")
+implementation("org.onebusaway:onebusaway-sdk-java:0.1.0-alpha.24")
```
-#### Maven
+### Maven
```xml
- org.onebusaway
- onebusaway-sdk-java
- 0.1.0-alpha.23
+ org.onebusaway
+ onebusaway-sdk-java
+ 0.1.0-alpha.24
```
-### Configure the client
+## Requirements
+
+This library requires Java 8 or later.
+
+## Usage
+
+```java
+import org.onebusaway.client.OnebusawaySdkClient;
+import org.onebusaway.client.okhttp.OnebusawaySdkOkHttpClient;
+import org.onebusaway.models.currenttime.CurrentTimeRetrieveParams;
+import org.onebusaway.models.currenttime.CurrentTimeRetrieveResponse;
+
+// Configures using the `ONEBUSAWAY_API_KEY` and `ONEBUSAWAY_SDK_BASE_URL` environment variables
+OnebusawaySdkClient client = OnebusawaySdkOkHttpClient.fromEnv();
+
+CurrentTimeRetrieveResponse currentTime = client.currentTime().retrieve();
+```
+
+## Client configuration
+
+Configure the client using environment variables:
+
+```java
+import org.onebusaway.client.OnebusawaySdkClient;
+import org.onebusaway.client.okhttp.OnebusawaySdkOkHttpClient;
+
+// Configures using the `ONEBUSAWAY_API_KEY` and `ONEBUSAWAY_SDK_BASE_URL` environment variables
+OnebusawaySdkClient client = OnebusawaySdkOkHttpClient.fromEnv();
+```
-Use `OnebusawaySdkOkHttpClient.builder()` to configure the client. At a minimum you need to set `.apiKey()`:
+Or manually:
```java
import org.onebusaway.client.OnebusawaySdkClient;
@@ -55,136 +82,176 @@ OnebusawaySdkClient client = OnebusawaySdkOkHttpClient.builder()
.build();
```
-Alternately, set the environment with `ONEBUSAWAY_API_KEY`, and use `OnebusawaySdkOkHttpClient.fromEnv()` to read from the environment.
+Or using a combination of the two approaches:
```java
-OnebusawaySdkClient client = OnebusawaySdkOkHttpClient.fromEnv();
+import org.onebusaway.client.OnebusawaySdkClient;
+import org.onebusaway.client.okhttp.OnebusawaySdkOkHttpClient;
-// Note: you can also call fromEnv() from the client builder, for example if you need to set additional properties
OnebusawaySdkClient client = OnebusawaySdkOkHttpClient.builder()
+ // Configures using the `ONEBUSAWAY_API_KEY` and `ONEBUSAWAY_SDK_BASE_URL` environment variables
.fromEnv()
- // ... set properties on the builder
+ .apiKey("My API Key")
.build();
```
-| Property | Environment variable | Required | Default value |
-| -------- | -------------------- | -------- | ------------- |
-| apiKey | `ONEBUSAWAY_API_KEY` | true | — |
+See this table for the available options:
-Read the documentation for more configuration options.
+| Setter | Environment variable | Required | Default value |
+| --------- | ------------------------- | -------- | ----------------------------------------- |
+| `apiKey` | `ONEBUSAWAY_API_KEY` | true | - |
+| `baseUrl` | `ONEBUSAWAY_SDK_BASE_URL` | true | `"https://api.pugetsound.onebusaway.org"` |
----
+> [!TIP]
+> Don't create more than one client in the same application. Each client has a connection pool and
+> thread pools, which are more efficient to share between requests.
-### Example: creating a resource
+## Requests and responses
-To create a new current time, first use the `CurrentTimeRetrieveParams` builder to specify attributes,
-then pass that to the `retrieve` method of the `currentTime` service.
+To send a request to the Onebusaway SDK API, build an instance of some `Params` class and pass it to the corresponding client method. When the response is received, it will be deserialized into an instance of a Java class.
-```java
-import org.onebusaway.models.CurrentTimeRetrieveParams;
-import org.onebusaway.models.CurrentTimeRetrieveResponse;
-
-CurrentTimeRetrieveParams params = CurrentTimeRetrieveParams.builder().build();
-CurrentTimeRetrieveResponse currentTime = client.currentTime().retrieve(params);
-```
+For example, `client.currentTime().retrieve(...)` should be called with an instance of `CurrentTimeRetrieveParams`, and it will return an instance of `CurrentTimeRetrieveResponse`.
----
+## Immutability
-## Requests
+Each class in the SDK has an associated [builder](https://blogs.oracle.com/javamagazine/post/exploring-joshua-blochs-builder-design-pattern-in-java) or factory method for constructing it.
-### Parameters and bodies
+Each class is [immutable](https://docs.oracle.com/javase/tutorial/essential/concurrency/immutable.html) once constructed. If the class has an associated builder, then it has a `toBuilder()` method, which can be used to convert it back to a builder for making a modified copy.
-To make a request to the Onebusaway SDK API, you generally build an instance of the appropriate `Params` class.
+Because each class is immutable, builder modification will _never_ affect already built class instances.
-In [Example: creating a resource](#example-creating-a-resource) above, we used the `CurrentTimeRetrieveParams.builder()` to pass to
-the `retrieve` method of the `currentTime` service.
+## Asynchronous execution
-Sometimes, the API may support other properties that are not yet supported in the Java SDK types. In that case,
-you can attach them using the `putAdditionalProperty` method.
+The default client is synchronous. To switch to asynchronous execution, call the `async()` method:
```java
-import org.onebusaway.models.core.JsonValue;
-CurrentTimeRetrieveParams params = CurrentTimeRetrieveParams.builder()
- // ... normal properties
- .putAdditionalProperty("secret_param", JsonValue.from("4242"))
- .build();
-```
+import java.util.concurrent.CompletableFuture;
+import org.onebusaway.client.OnebusawaySdkClient;
+import org.onebusaway.client.okhttp.OnebusawaySdkOkHttpClient;
+import org.onebusaway.models.currenttime.CurrentTimeRetrieveParams;
+import org.onebusaway.models.currenttime.CurrentTimeRetrieveResponse;
-## Responses
+// Configures using the `ONEBUSAWAY_API_KEY` and `ONEBUSAWAY_SDK_BASE_URL` environment variables
+OnebusawaySdkClient client = OnebusawaySdkOkHttpClient.fromEnv();
-### Response validation
+CompletableFuture currentTime = client.async().currentTime().retrieve();
+```
-When receiving a response, the Onebusaway SDK Java SDK will deserialize it into instances of the typed model classes. In rare cases, the API may return a response property that doesn't match the expected Java type. If you directly access the mistaken property, the SDK will throw an unchecked `OnebusawaySdkInvalidDataException` at runtime. If you would prefer to check in advance that that response is completely well-typed, call `.validate()` on the returned model.
+Or create an asynchronous client from the beginning:
```java
-CurrentTimeRetrieveResponse currentTime = client.currentTime().retrieve().validate();
+import java.util.concurrent.CompletableFuture;
+import org.onebusaway.client.OnebusawaySdkClientAsync;
+import org.onebusaway.client.okhttp.OnebusawaySdkOkHttpClientAsync;
+import org.onebusaway.models.currenttime.CurrentTimeRetrieveParams;
+import org.onebusaway.models.currenttime.CurrentTimeRetrieveResponse;
+
+// Configures using the `ONEBUSAWAY_API_KEY` and `ONEBUSAWAY_SDK_BASE_URL` environment variables
+OnebusawaySdkClientAsync client = OnebusawaySdkOkHttpClientAsync.fromEnv();
+
+CompletableFuture currentTime = client.currentTime().retrieve();
```
-### Response properties as JSON
+The asynchronous client supports the same options as the synchronous one, except most methods return `CompletableFuture`s.
-In rare cases, you may want to access the underlying JSON value for a response property rather than using the typed version provided by
-this SDK. Each model property has a corresponding JSON version, with an underscore before the method name, which returns a `JsonField` value.
+## Raw responses
+
+The SDK defines methods that deserialize responses into instances of Java classes. However, these methods don't provide access to the response headers, status code, or the raw response body.
+
+To access this data, prefix any HTTP method call on a client or service with `withRawResponse()`:
```java
-JsonField field = responseObj._field();
+import org.onebusaway.core.http.Headers;
+import org.onebusaway.core.http.HttpResponseFor;
+import org.onebusaway.models.currenttime.CurrentTimeRetrieveParams;
+import org.onebusaway.models.currenttime.CurrentTimeRetrieveResponse;
-if (field.isMissing()) {
- // Value was not specified in the JSON response
-} else if (field.isNull()) {
- // Value was provided as a literal null
-} else {
- // See if value was provided as a string
- Optional jsonString = field.asString();
+HttpResponseFor currentTime = client.currentTime().withRawResponse().retrieve();
- // If the value given by the API did not match the shape that the SDK expects
- // you can deserialise into a custom type
- MyClass myObj = responseObj._field().asUnknown().orElseThrow().convert(MyClass.class);
-}
+int statusCode = currentTime.statusCode();
+Headers headers = currentTime.headers();
```
-### Additional model properties
-
-Sometimes, the server response may include additional properties that are not yet available in this library's types. You can access them using the model's `_additionalProperties` method:
+You can still deserialize the response into an instance of a Java class if needed:
```java
-JsonValue secret = references._additionalProperties().get("secret_field");
+import org.onebusaway.models.currenttime.CurrentTimeRetrieveResponse;
+
+CurrentTimeRetrieveResponse parsedCurrentTime = currentTime.parse();
```
----
+## Error handling
----
+The SDK throws custom unchecked exception types:
-## Error handling
+- [`OnebusawaySdkServiceException`](onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/errors/OnebusawaySdkServiceException.kt): Base class for HTTP errors. See this table for which exception subclass is thrown for each HTTP status code:
-This library throws exceptions in a single hierarchy for easy handling:
+ | Status | Exception |
+ | ------ | ---------------------------------------------------------------------------------------------------------------------------------- |
+ | 400 | [`BadRequestException`](onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/errors/BadRequestException.kt) |
+ | 401 | [`UnauthorizedException`](onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/errors/UnauthorizedException.kt) |
+ | 403 | [`PermissionDeniedException`](onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/errors/PermissionDeniedException.kt) |
+ | 404 | [`NotFoundException`](onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/errors/NotFoundException.kt) |
+ | 422 | [`UnprocessableEntityException`](onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/errors/UnprocessableEntityException.kt) |
+ | 429 | [`RateLimitException`](onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/errors/RateLimitException.kt) |
+ | 5xx | [`InternalServerException`](onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/errors/InternalServerException.kt) |
+ | others | [`UnexpectedStatusCodeException`](onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/errors/UnexpectedStatusCodeException.kt) |
+
+- [`OnebusawaySdkIoException`](onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/errors/OnebusawaySdkIoException.kt): I/O networking errors.
+
+- [`OnebusawaySdkInvalidDataException`](onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/errors/OnebusawaySdkInvalidDataException.kt): Failure to interpret successfully parsed data. For example, when accessing a property that's supposed to be required, but the API unexpectedly omitted it from the response.
+
+- [`OnebusawaySdkException`](onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/errors/OnebusawaySdkException.kt): Base class for all exceptions. Most errors will result in one of the previously mentioned ones, but completely generic errors may be thrown using the base class.
+
+## Logging
+
+The SDK uses the standard [OkHttp logging interceptor](https://github.com/square/okhttp/tree/master/okhttp-logging-interceptor).
+
+Enable logging by setting the `ONEBUSAWAY_SDK_LOG` environment variable to `info`:
+
+```sh
+$ export ONEBUSAWAY_SDK_LOG=info
+```
+
+Or to `debug` for more verbose logging:
+
+```sh
+$ export ONEBUSAWAY_SDK_LOG=debug
+```
-- **`OnebusawaySdkException`** - Base exception for all exceptions
+## Jackson
- - **`OnebusawaySdkServiceException`** - HTTP errors with a well-formed response body we were able to parse. The exception message and the `.debuggingRequestId()` will be set by the server.
+The SDK depends on [Jackson](https://github.com/FasterXML/jackson) for JSON serialization/deserialization. It is compatible with version 2.13.4 or higher, but depends on version 2.18.2 by default.
- | 400 | BadRequestException |
- | ------ | ----------------------------- |
- | 401 | AuthenticationException |
- | 403 | PermissionDeniedException |
- | 404 | NotFoundException |
- | 422 | UnprocessableEntityException |
- | 429 | RateLimitException |
- | 5xx | InternalServerException |
- | others | UnexpectedStatusCodeException |
+The SDK throws an exception if it detects an incompatible Jackson version at runtime (e.g. if the default version was overridden in your Maven or Gradle config).
- - **`OnebusawaySdkIoException`** - I/O networking errors
- - **`OnebusawaySdkInvalidDataException`** - any other exceptions on the client side, e.g.:
- - We failed to serialize the request body
- - We failed to parse the response body (has access to response code and body)
+If the SDK threw an exception, but you're _certain_ the version is compatible, then disable the version check using the `checkJacksonVersionCompatibility` on [`OnebusawaySdkOkHttpClient`](onebusaway-sdk-java-client-okhttp/src/main/kotlin/org/onebusaway/client/okhttp/OnebusawaySdkOkHttpClient.kt) or [`OnebusawaySdkOkHttpClientAsync`](onebusaway-sdk-java-client-okhttp/src/main/kotlin/org/onebusaway/client/okhttp/OnebusawaySdkOkHttpClientAsync.kt).
+
+> [!CAUTION]
+> We make no guarantee that the SDK works correctly when the Jackson version check is disabled.
## Network options
### Retries
-Requests that experience certain errors are automatically retried 2 times by default, with a short exponential backoff. Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict, 429 Rate Limit, and >=500 Internal errors will all be retried by default.
-You can provide a `maxRetries` on the client builder to configure this:
+The SDK automatically retries 2 times by default, with a short exponential backoff.
+
+Only the following error types are retried:
+
+- Connection errors (for example, due to a network connectivity problem)
+- 408 Request Timeout
+- 409 Conflict
+- 429 Rate Limit
+- 5xx Internal
+
+The API may also explicitly instruct the SDK to retry or not retry a response.
+
+To set a custom number of retries, configure the client using the `maxRetries` method:
```java
+import org.onebusaway.client.OnebusawaySdkClient;
+import org.onebusaway.client.okhttp.OnebusawaySdkOkHttpClient;
+
OnebusawaySdkClient client = OnebusawaySdkOkHttpClient.builder()
.fromEnv()
.maxRetries(4)
@@ -193,9 +260,23 @@ OnebusawaySdkClient client = OnebusawaySdkOkHttpClient.builder()
### Timeouts
-Requests time out after 1 minute by default. You can configure this on the client builder:
+Requests time out after 1 minute by default.
+
+To set a custom timeout, configure the method call using the `timeout` method:
```java
+import org.onebusaway.models.currenttime.CurrentTimeRetrieveResponse;
+
+CurrentTimeRetrieveResponse currentTime = client.currentTime().retrieve(RequestOptions.builder().timeout(Duration.ofSeconds(30)).build());
+```
+
+Or configure the default for all method calls at the client level:
+
+```java
+import java.time.Duration;
+import org.onebusaway.client.OnebusawaySdkClient;
+import org.onebusaway.client.okhttp.OnebusawaySdkOkHttpClient;
+
OnebusawaySdkClient client = OnebusawaySdkOkHttpClient.builder()
.fromEnv()
.timeout(Duration.ofSeconds(30))
@@ -204,53 +285,266 @@ OnebusawaySdkClient client = OnebusawaySdkOkHttpClient.builder()
### Proxies
-Requests can be routed through a proxy. You can configure this on the client builder:
+To route requests through a proxy, configure the client using the `proxy` method:
```java
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import org.onebusaway.client.OnebusawaySdkClient;
+import org.onebusaway.client.okhttp.OnebusawaySdkOkHttpClient;
+
OnebusawaySdkClient client = OnebusawaySdkOkHttpClient.builder()
.fromEnv()
.proxy(new Proxy(
- Type.HTTP,
- new InetSocketAddress("proxy.com", 8080)
+ Proxy.Type.HTTP, new InetSocketAddress(
+ "https://example.com", 8080
+ )
))
.build();
```
-## Making custom/undocumented requests
+### Custom HTTP client
-This library is typed for convenient access to the documented API. If you need to access undocumented
-params or response properties, the library can still be used.
+The SDK consists of three artifacts:
-### Undocumented request params
+- `onebusaway-sdk-java-core`
+ - Contains core SDK logic
+ - Does not depend on [OkHttp](https://square.github.io/okhttp)
+ - Exposes [`OnebusawaySdkClient`](onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/client/OnebusawaySdkClient.kt), [`OnebusawaySdkClientAsync`](onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/client/OnebusawaySdkClientAsync.kt), [`OnebusawaySdkClientImpl`](onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/client/OnebusawaySdkClientImpl.kt), and [`OnebusawaySdkClientAsyncImpl`](onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/client/OnebusawaySdkClientAsyncImpl.kt), all of which can work with any HTTP client
+- `onebusaway-sdk-java-client-okhttp`
+ - Depends on [OkHttp](https://square.github.io/okhttp)
+ - Exposes [`OnebusawaySdkOkHttpClient`](onebusaway-sdk-java-client-okhttp/src/main/kotlin/org/onebusaway/client/okhttp/OnebusawaySdkOkHttpClient.kt) and [`OnebusawaySdkOkHttpClientAsync`](onebusaway-sdk-java-client-okhttp/src/main/kotlin/org/onebusaway/client/okhttp/OnebusawaySdkOkHttpClientAsync.kt), which provide a way to construct [`OnebusawaySdkClientImpl`](onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/client/OnebusawaySdkClientImpl.kt) and [`OnebusawaySdkClientAsyncImpl`](onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/client/OnebusawaySdkClientAsyncImpl.kt), respectively, using OkHttp
+- `onebusaway-sdk-java`
+ - Depends on and exposes the APIs of both `onebusaway-sdk-java-core` and `onebusaway-sdk-java-client-okhttp`
+ - Does not have its own logic
-To make requests using undocumented parameters, you can provide or override parameters on the params object
-while building it.
+This structure allows replacing the SDK's default HTTP client without pulling in unnecessary dependencies.
-```kotlin
-FooCreateParams address = FooCreateParams.builder()
- .id("my_id")
- .putAdditionalProperty("secret_prop", JsonValue.from("hello"))
+#### Customized [`OkHttpClient`](https://square.github.io/okhttp/3.x/okhttp/okhttp3/OkHttpClient.html)
+
+> [!TIP]
+> Try the available [network options](#network-options) before replacing the default client.
+
+To use a customized `OkHttpClient`:
+
+1. Replace your [`onebusaway-sdk-java` dependency](#installation) with `onebusaway-sdk-java-core`
+2. Copy `onebusaway-sdk-java-client-okhttp`'s [`OkHttpClient`](onebusaway-sdk-java-client-okhttp/src/main/kotlin/org/onebusaway/client/okhttp/OkHttpClient.kt) class into your code and customize it
+3. Construct [`OnebusawaySdkClientImpl`](onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/client/OnebusawaySdkClientImpl.kt) or [`OnebusawaySdkClientAsyncImpl`](onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/client/OnebusawaySdkClientAsyncImpl.kt), similarly to [`OnebusawaySdkOkHttpClient`](onebusaway-sdk-java-client-okhttp/src/main/kotlin/org/onebusaway/client/okhttp/OnebusawaySdkOkHttpClient.kt) or [`OnebusawaySdkOkHttpClientAsync`](onebusaway-sdk-java-client-okhttp/src/main/kotlin/org/onebusaway/client/okhttp/OnebusawaySdkOkHttpClientAsync.kt), using your customized client
+
+### Completely custom HTTP client
+
+To use a completely custom HTTP client:
+
+1. Replace your [`onebusaway-sdk-java` dependency](#installation) with `onebusaway-sdk-java-core`
+2. Write a class that implements the [`HttpClient`](onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/HttpClient.kt) interface
+3. Construct [`OnebusawaySdkClientImpl`](onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/client/OnebusawaySdkClientImpl.kt) or [`OnebusawaySdkClientAsyncImpl`](onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/client/OnebusawaySdkClientAsyncImpl.kt), similarly to [`OnebusawaySdkOkHttpClient`](onebusaway-sdk-java-client-okhttp/src/main/kotlin/org/onebusaway/client/okhttp/OnebusawaySdkOkHttpClient.kt) or [`OnebusawaySdkOkHttpClientAsync`](onebusaway-sdk-java-client-okhttp/src/main/kotlin/org/onebusaway/client/okhttp/OnebusawaySdkOkHttpClientAsync.kt), using your new client class
+
+## Undocumented API functionality
+
+The SDK is typed for convenient usage of the documented API. However, it also supports working with undocumented or not yet supported parts of the API.
+
+### Parameters
+
+To set undocumented parameters, call the `putAdditionalHeader`, `putAdditionalQueryParam`, or `putAdditionalBodyProperty` methods on any `Params` class:
+
+```java
+import org.onebusaway.core.JsonValue;
+import org.onebusaway.models.currenttime.CurrentTimeRetrieveParams;
+
+CurrentTimeRetrieveParams params = CurrentTimeRetrieveParams.builder()
+ .putAdditionalHeader("Secret-Header", "42")
+ .putAdditionalQueryParam("secret_query_param", "42")
+ .putAdditionalBodyProperty("secretProperty", JsonValue.from("42"))
+ .build();
+```
+
+These can be accessed on the built object later using the `_additionalHeaders()`, `_additionalQueryParams()`, and `_additionalBodyProperties()` methods.
+
+To set a documented parameter or property to an undocumented or not yet supported _value_, pass a [`JsonValue`](onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/Values.kt) object to its setter:
+
+```java
+import org.onebusaway.models.currenttime.CurrentTimeRetrieveParams;
+
+CurrentTimeRetrieveParams params = CurrentTimeRetrieveParams.builder().build();
+```
+
+The most straightforward way to create a [`JsonValue`](onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/Values.kt) is using its `from(...)` method:
+
+```java
+import java.util.List;
+import java.util.Map;
+import org.onebusaway.core.JsonValue;
+
+// Create primitive JSON values
+JsonValue nullValue = JsonValue.from(null);
+JsonValue booleanValue = JsonValue.from(true);
+JsonValue numberValue = JsonValue.from(42);
+JsonValue stringValue = JsonValue.from("Hello World!");
+
+// Create a JSON array value equivalent to `["Hello", "World"]`
+JsonValue arrayValue = JsonValue.from(List.of(
+ "Hello", "World"
+));
+
+// Create a JSON object value equivalent to `{ "a": 1, "b": 2 }`
+JsonValue objectValue = JsonValue.from(Map.of(
+ "a", 1,
+ "b", 2
+));
+
+// Create an arbitrarily nested JSON equivalent to:
+// {
+// "a": [1, 2],
+// "b": [3, 4]
+// }
+JsonValue complexValue = JsonValue.from(Map.of(
+ "a", List.of(
+ 1, 2
+ ),
+ "b", List.of(
+ 3, 4
+ )
+));
+```
+
+Normally a `Builder` class's `build` method will throw [`IllegalStateException`](https://docs.oracle.com/javase/8/docs/api/java/lang/IllegalStateException.html) if any required parameter or property is unset.
+
+To forcibly omit a required parameter or property, pass [`JsonMissing`](onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/Values.kt):
+
+```java
+import org.onebusaway.core.JsonMissing;
+import org.onebusaway.models.agency.AgencyRetrieveParams;
+import org.onebusaway.models.currenttime.CurrentTimeRetrieveParams;
+
+CurrentTimeRetrieveParams params = AgencyRetrieveParams.builder()
+ .agencyId(JsonMissing.of())
.build();
```
-### Undocumented response properties
+### Response properties
+
+To access undocumented response properties, call the `_additionalProperties()` method:
+
+```java
+import java.util.Map;
+import org.onebusaway.core.JsonValue;
+
+Map additionalProperties = client.currentTime().retrieve(params)._additionalProperties();
+JsonValue secretPropertyValue = additionalProperties.get("secretProperty");
+
+String result = secretPropertyValue.accept(new JsonValue.Visitor<>() {
+ @Override
+ public String visitNull() {
+ return "It's null!";
+ }
+
+ @Override
+ public String visitBoolean(boolean value) {
+ return "It's a boolean!";
+ }
+
+ @Override
+ public String visitNumber(Number value) {
+ return "It's a number!";
+ }
+
+ // Other methods include `visitMissing`, `visitString`, `visitArray`, and `visitObject`
+ // The default implementation of each unimplemented method delegates to `visitDefault`, which throws by default, but can also be overridden
+});
+```
+
+To access a property's raw JSON value, which may be undocumented, call its `_` prefixed method:
+
+```java
+import java.util.Optional;
+import org.onebusaway.core.JsonField;
-To access undocumented response properties, you can use `res._additionalProperties()` on a response object to
-get a map of untyped fields of type `Map`. You can then access fields like
-`._additionalProperties().get("secret_prop").asString()` or use other helpers defined on the `JsonValue` class
-to extract it to a desired type.
+JsonField field = client.currentTime().retrieve(params)._field();
+
+if (field.isMissing()) {
+ // The property is absent from the JSON response
+} else if (field.isNull()) {
+ // The property was set to literal null
+} else {
+ // Check if value was provided as a string
+ // Other methods include `asNumber()`, `asBoolean()`, etc.
+ Optional jsonString = field.asString();
+
+ // Try to deserialize into a custom type
+ MyClass myObject = field.asUnknown().orElseThrow().convert(MyClass.class);
+}
+```
+
+### Response validation
+
+In rare cases, the API may return a response that doesn't match the expected type. For example, the SDK may expect a property to contain a `String`, but the API could return something else.
+
+By default, the SDK will not throw an exception in this case. It will throw [`OnebusawaySdkInvalidDataException`](onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/errors/OnebusawaySdkInvalidDataException.kt) only if you directly access the property.
+
+If you would prefer to check that the response is completely well-typed upfront, then either call `validate()`:
+
+```java
+import org.onebusaway.models.currenttime.CurrentTimeRetrieveResponse;
+
+CurrentTimeRetrieveResponse currentTime = client.currentTime().retrieve(params).validate();
+```
+
+Or configure the method call to validate the response using the `responseValidation` method:
+
+```java
+import org.onebusaway.models.currenttime.CurrentTimeRetrieveResponse;
+
+CurrentTimeRetrieveResponse currentTime = client.currentTime().retrieve(RequestOptions.builder().responseValidation(true).build());
+```
+
+Or configure the default for all method calls at the client level:
+
+```java
+import org.onebusaway.client.OnebusawaySdkClient;
+import org.onebusaway.client.okhttp.OnebusawaySdkOkHttpClient;
+
+OnebusawaySdkClient client = OnebusawaySdkOkHttpClient.builder()
+ .fromEnv()
+ .responseValidation(true)
+ .build();
+```
+
+## FAQ
+
+### Why don't you use plain `enum` classes?
+
+Java `enum` classes are not trivially [forwards compatible](https://www.stainless.com/blog/making-java-enums-forwards-compatible). Using them in the SDK could cause runtime exceptions if the API is updated to respond with a new enum value.
+
+### Why do you represent fields using `JsonField` instead of just plain `T`?
+
+Using `JsonField` enables a few features:
+
+- Allowing usage of [undocumented API functionality](#undocumented-api-functionality)
+- Lazily [validating the API response against the expected shape](#response-validation)
+- Representing absent vs explicitly null values
+
+### Why don't you use [`data` classes](https://kotlinlang.org/docs/data-classes.html)?
+
+It is not [backwards compatible to add new fields to a data class](https://kotlinlang.org/docs/api-guidelines-backward-compatibility.html#avoid-using-data-classes-in-your-api) and we don't want to introduce a breaking change every time we add a field to a class.
+
+### Why don't you use checked exceptions?
+
+Checked exceptions are widely considered a mistake in the Java programming language. In fact, they were omitted from Kotlin for this reason.
+
+Checked exceptions:
+
+- Are verbose to handle
+- Encourage error handling at the wrong level of abstraction, where nothing can be done about the error
+- Are tedious to propagate due to the [function coloring problem](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function)
+- Don't play well with lambdas (also due to the function coloring problem)
## Semantic versioning
This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions:
-1. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals)_.
+1. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_
2. Changes that we do not expect to impact the vast majority of users in practice.
We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience.
We are keen for your feedback; please open an [issue](https://www.github.com/OneBusAway/java-sdk/issues) with questions, bugs, or suggestions.
-
-## Requirements
-
-This library requires Java 8 or later.
diff --git a/SECURITY.md b/SECURITY.md
index 62ac016..8be07dc 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -2,9 +2,9 @@
## Reporting Security Issues
-This SDK is generated by [Stainless Software Inc](http://stainlessapi.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken.
+This SDK is generated by [Stainless Software Inc](http://stainless.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken.
-To report a security issue, please contact the Stainless team at security@stainlessapi.com.
+To report a security issue, please contact the Stainless team at security@stainless.com.
## Responsible Disclosure
diff --git a/build.gradle.kts b/build.gradle.kts
index 8c8ea48..a036b20 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,10 +1,23 @@
plugins {
+ id("org.jetbrains.dokka") version "2.0.0"
+}
+repositories {
+ mavenCentral()
}
allprojects {
group = "org.onebusaway"
- version = "0.1.0-alpha.23" // x-release-please-version
+ version = "0.1.0-alpha.24" // x-release-please-version
}
+subprojects {
+ apply(plugin = "org.jetbrains.dokka")
+}
+// Avoid race conditions between `dokkaJavadocCollector` and `dokkaJavadocJar` tasks
+tasks.named("dokkaJavadocCollector").configure {
+ subprojects.flatMap { it.tasks }
+ .filter { it.project.name != "onebusaway-sdk-java" && it.name == "dokkaJavadocJar" }
+ .forEach { mustRunAfter(it) }
+}
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
index 493cb32..778c89d 100644
--- a/buildSrc/build.gradle.kts
+++ b/buildSrc/build.gradle.kts
@@ -1,6 +1,6 @@
plugins {
`kotlin-dsl`
- kotlin("jvm") version "1.9.22"
+ kotlin("jvm") version "1.9.20"
id("com.vanniktech.maven.publish") version "0.28.0"
}
@@ -10,7 +10,7 @@ repositories {
}
dependencies {
- implementation("com.diffplug.spotless:spotless-plugin-gradle:6.25.0")
- implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.23")
+ implementation("com.diffplug.spotless:spotless-plugin-gradle:7.0.2")
+ implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20")
implementation("com.vanniktech:gradle-maven-publish-plugin:0.28.0")
}
diff --git a/buildSrc/src/main/kotlin/onebusaway-sdk.java.gradle.kts b/buildSrc/src/main/kotlin/onebusaway-sdk.java.gradle.kts
index 32a150e..dfbacb8 100644
--- a/buildSrc/src/main/kotlin/onebusaway-sdk.java.gradle.kts
+++ b/buildSrc/src/main/kotlin/onebusaway-sdk.java.gradle.kts
@@ -1,9 +1,5 @@
import com.diffplug.gradle.spotless.SpotlessExtension
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
-import com.vanniktech.maven.publish.JavaLibrary
-import com.vanniktech.maven.publish.JavadocJar
-import com.vanniktech.maven.publish.MavenPublishBaseExtension
-import com.vanniktech.maven.publish.SonatypeHost
plugins {
`java-library`
@@ -25,8 +21,11 @@ configure {
java {
toolchain {
- languageVersion.set(JavaLanguageVersion.of(17))
+ languageVersion.set(JavaLanguageVersion.of(21))
}
+
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
}
tasks.withType().configureEach {
@@ -43,9 +42,13 @@ tasks.named("jar") {
}
}
-tasks.named("test") {
+tasks.withType().configureEach {
useJUnitPlatform()
+ // Run tests in parallel to some degree.
+ maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
+ forkEvery = 100
+
testLogging {
exceptionFormat = TestExceptionFormat.FULL
}
diff --git a/buildSrc/src/main/kotlin/onebusaway-sdk.kotlin.gradle.kts b/buildSrc/src/main/kotlin/onebusaway-sdk.kotlin.gradle.kts
index 6da1ac7..be0b5d4 100644
--- a/buildSrc/src/main/kotlin/onebusaway-sdk.kotlin.gradle.kts
+++ b/buildSrc/src/main/kotlin/onebusaway-sdk.kotlin.gradle.kts
@@ -1,6 +1,6 @@
import com.diffplug.gradle.spotless.SpotlessExtension
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
-import com.vanniktech.maven.publish.*
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
plugins {
id("onebusaway-sdk.java")
@@ -9,7 +9,21 @@ plugins {
kotlin {
jvmToolchain {
- languageVersion.set(JavaLanguageVersion.of(17))
+ languageVersion.set(JavaLanguageVersion.of(21))
+ }
+
+ compilerOptions {
+ freeCompilerArgs = listOf(
+ "-Xjvm-default=all",
+ "-Xjdk-release=1.8",
+ // Suppress deprecation warnings because we may still reference and test deprecated members.
+ // TODO: Replace with `-Xsuppress-warning=DEPRECATION` once we use Kotlin compiler 2.1.0+.
+ "-nowarn",
+ )
+ jvmTarget.set(JvmTarget.JVM_1_8)
+ languageVersion.set(KotlinVersion.KOTLIN_1_8)
+ apiVersion.set(KotlinVersion.KOTLIN_1_8)
+ coreLibrariesVersion = "1.8.0"
}
}
@@ -20,10 +34,7 @@ configure {
}
}
-tasks.withType().configureEach {
- kotlinOptions {
- allWarningsAsErrors = true
- freeCompilerArgs = listOf("-Xjvm-default=all", "-Xjdk-release=1.8")
- jvmTarget = "1.8"
- }
+tasks.withType().configureEach {
+ systemProperty("junit.jupiter.execution.parallel.enabled", true)
+ systemProperty("junit.jupiter.execution.parallel.mode.default", "concurrent")
}
diff --git a/buildSrc/src/main/kotlin/onebusaway-sdk.publish.gradle.kts b/buildSrc/src/main/kotlin/onebusaway-sdk.publish.gradle.kts
index 4c6d7fa..8ba555d 100644
--- a/buildSrc/src/main/kotlin/onebusaway-sdk.publish.gradle.kts
+++ b/buildSrc/src/main/kotlin/onebusaway-sdk.publish.gradle.kts
@@ -1,10 +1,5 @@
-import org.gradle.api.publish.PublishingExtension
-import org.gradle.api.publish.maven.MavenPublication
-import org.gradle.kotlin.dsl.configure
-import org.gradle.kotlin.dsl.register
-import org.gradle.kotlin.dsl.get
-import com.vanniktech.maven.publish.JavaLibrary
import com.vanniktech.maven.publish.JavadocJar
+import com.vanniktech.maven.publish.KotlinJvm
import com.vanniktech.maven.publish.MavenPublishBaseExtension
import com.vanniktech.maven.publish.SonatypeHost
@@ -25,7 +20,13 @@ configure {
signAllPublications()
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
- this.coordinates(project.group.toString(), project.name, project.version.toString())
+ coordinates(project.group.toString(), project.name, project.version.toString())
+ configure(
+ KotlinJvm(
+ javadocJar = JavadocJar.Dokka("dokkaJavadoc"),
+ sourcesJar = true,
+ )
+ )
pom {
name.set("OneBusAway")
diff --git a/gradle.properties b/gradle.properties
index a3bc58f..ff76593 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,17 @@
org.gradle.caching=true
-org.gradle.jvmargs=-Xmx4g
+org.gradle.configuration-cache=true
org.gradle.parallel=true
-kotlin.daemon.jvmargs=-Xmx4g
+org.gradle.daemon=false
+# These options improve our compilation and test performance. They are inherited by the Kotlin daemon.
+org.gradle.jvmargs=\
+ -Xms1g \
+ -Xmx4g \
+ -XX:+UseParallelGC \
+ -XX:InitialCodeCacheSize=256m \
+ -XX:ReservedCodeCacheSize=1G \
+ -XX:MetaspaceSize=256m \
+ -XX:TieredStopAtLevel=1 \
+ -XX:GCTimeRatio=4 \
+ -XX:CICompilerCount=4 \
+ -XX:+OptimizeStringConcat \
+ -XX:+UseStringDeduplication
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index e644113..a4b76b9 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index b82aa23..cea7a79 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/gradlew b/gradlew
index 1aa94a4..f3b75f3 100755
--- a/gradlew
+++ b/gradlew
@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
+# SPDX-License-Identifier: Apache-2.0
+#
##############################################################################
#
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
-# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -84,7 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
-APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
diff --git a/gradlew.bat b/gradlew.bat
index 25da30d..9d21a21 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
diff --git a/onebusaway-sdk-java-client-okhttp/build.gradle.kts b/onebusaway-sdk-java-client-okhttp/build.gradle.kts
index 037ef81..2fd0abf 100644
--- a/onebusaway-sdk-java-client-okhttp/build.gradle.kts
+++ b/onebusaway-sdk-java-client-okhttp/build.gradle.kts
@@ -6,10 +6,9 @@ plugins {
dependencies {
api(project(":onebusaway-sdk-java-core"))
- implementation("com.google.guava:guava:33.0.0-jre")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
+ implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
testImplementation(kotlin("test"))
testImplementation("org.assertj:assertj-core:3.25.3")
- testImplementation("org.slf4j:slf4j-simple:2.0.12")
}
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 427df17..32d8d5e 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
@@ -1,7 +1,5 @@
package org.onebusaway.client.okhttp
-import com.google.common.collect.ListMultimap
-import com.google.common.collect.MultimapBuilder
import java.io.IOException
import java.io.InputStream
import java.net.Proxy
@@ -9,17 +7,21 @@ import java.time.Duration
import java.util.concurrent.CompletableFuture
import okhttp3.Call
import okhttp3.Callback
-import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.Interceptor
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
+import okhttp3.logging.HttpLoggingInterceptor
import okio.BufferedSink
import org.onebusaway.core.RequestOptions
+import org.onebusaway.core.Timeout
+import org.onebusaway.core.checkRequired
+import org.onebusaway.core.http.Headers
import org.onebusaway.core.http.HttpClient
import org.onebusaway.core.http.HttpMethod
import org.onebusaway.core.http.HttpRequest
@@ -31,22 +33,8 @@ class OkHttpClient
private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val baseUrl: HttpUrl) :
HttpClient {
- private fun getClient(requestOptions: RequestOptions): okhttp3.OkHttpClient {
- val timeout = requestOptions.timeout ?: return okHttpClient
- return okHttpClient
- .newBuilder()
- .connectTimeout(timeout)
- .readTimeout(timeout)
- .writeTimeout(timeout)
- .callTimeout(if (timeout.seconds == 0L) timeout else timeout.plusSeconds(30))
- .build()
- }
-
- override fun execute(
- request: HttpRequest,
- requestOptions: RequestOptions,
- ): HttpResponse {
- val call = getClient(requestOptions).newCall(request.toRequest())
+ override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse {
+ val call = newCall(request, requestOptions)
return try {
call.execute().toResponse()
@@ -65,18 +53,18 @@ private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val
request.body?.run { future.whenComplete { _, _ -> close() } }
- val call = getClient(requestOptions).newCall(request.toRequest())
- call.enqueue(
- object : Callback {
- override fun onResponse(call: Call, response: Response) {
- future.complete(response.toResponse())
- }
+ newCall(request, requestOptions)
+ .enqueue(
+ object : Callback {
+ override fun onResponse(call: Call, response: Response) {
+ future.complete(response.toResponse())
+ }
- override fun onFailure(call: Call, e: IOException) {
- future.completeExceptionally(OnebusawaySdkIoException("Request failed", e))
+ override fun onFailure(call: Call, e: IOException) {
+ future.completeExceptionally(OnebusawaySdkIoException("Request failed", e))
+ }
}
- }
- )
+ )
return future
}
@@ -87,19 +75,72 @@ private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val
okHttpClient.cache?.close()
}
- private fun HttpRequest.toRequest(): Request {
+ private fun newCall(request: HttpRequest, requestOptions: RequestOptions): Call {
+ val clientBuilder = okHttpClient.newBuilder()
+
+ // Custom logging interceptor for URL logging
+ clientBuilder.addNetworkInterceptor(LoggingInterceptor())
+
+ val logLevel =
+ when (System.getenv("ONEBUSAWAY_SDK_LOG")?.lowercase()) {
+ "info" -> HttpLoggingInterceptor.Level.BASIC
+ "debug" -> HttpLoggingInterceptor.Level.BODY
+ else -> null
+ }
+ if (logLevel != null) {
+ clientBuilder.addNetworkInterceptor(HttpLoggingInterceptor().setLevel(logLevel))
+ }
+
+ requestOptions.timeout?.let {
+ clientBuilder
+ .connectTimeout(it.connect())
+ .readTimeout(it.read())
+ .writeTimeout(it.write())
+ .callTimeout(it.request())
+ }
+
+ val client = clientBuilder.build()
+ return client.newCall(request.toRequest(client))
+ }
+
+ private fun HttpRequest.toRequest(client: okhttp3.OkHttpClient): Request {
var body: RequestBody? = body?.toRequestBody()
- // OkHttpClient always requires a request body for PUT and POST methods.
- if (body == null && (method == HttpMethod.PUT || method == HttpMethod.POST)) {
+ if (body == null && requiresBody(method)) {
body = "".toRequestBody()
}
val builder = Request.Builder().url(toUrl()).method(method.name, body)
- headers.forEach(builder::header)
+ headers.names().forEach { name ->
+ headers.values(name).forEach { builder.header(name, it) }
+ }
+
+ if (
+ !headers.names().contains("X-Stainless-Read-Timeout") && client.readTimeoutMillis != 0
+ ) {
+ builder.header(
+ "X-Stainless-Read-Timeout",
+ Duration.ofMillis(client.readTimeoutMillis.toLong()).seconds.toString(),
+ )
+ }
+ if (!headers.names().contains("X-Stainless-Timeout") && client.callTimeoutMillis != 0) {
+ builder.header(
+ "X-Stainless-Timeout",
+ Duration.ofMillis(client.callTimeoutMillis.toLong()).seconds.toString(),
+ )
+ }
return builder.build()
}
+ /** `OkHttpClient` always requires a request body for some methods. */
+ private fun requiresBody(method: HttpMethod): Boolean =
+ when (method) {
+ HttpMethod.POST,
+ HttpMethod.PUT,
+ HttpMethod.PATCH -> true
+ else -> false
+ }
+
private fun HttpRequest.toUrl(): String {
url?.let {
return it
@@ -107,7 +148,9 @@ private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val
val builder = baseUrl.newBuilder()
pathSegments.forEach(builder::addPathSegment)
- queryParams.forEach(builder::addQueryParameter)
+ queryParams.keys().forEach { key ->
+ queryParams.values(key).forEach { builder.addQueryParameter(key, it) }
+ }
return builder.toString()
}
@@ -133,7 +176,7 @@ private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val
return object : HttpResponse {
override fun statusCode(): Int = code
- override fun headers(): ListMultimap = headers
+ override fun headers(): Headers = headers
override fun body(): InputStream = body!!.byteStream()
@@ -141,42 +184,51 @@ private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val
}
}
- private fun Headers.toHeaders(): ListMultimap {
- val headers =
- MultimapBuilder.treeKeys(String.CASE_INSENSITIVE_ORDER)
- .arrayListValues()
- .build()
- forEach { pair -> headers.put(pair.first, pair.second) }
- return headers
+ private fun okhttp3.Headers.toHeaders(): Headers {
+ val headersBuilder = Headers.builder()
+ forEach { (name, value) -> headersBuilder.put(name, value) }
+ return headersBuilder.build()
}
companion object {
@JvmStatic fun builder() = Builder()
}
- class Builder {
+ class Builder internal constructor() {
private var baseUrl: HttpUrl? = null
- // The default timeout is 1 minute.
- private var timeout: Duration = Duration.ofSeconds(60)
+ private var timeout: Timeout = Timeout.default()
private var proxy: Proxy? = null
fun baseUrl(baseUrl: String) = apply { this.baseUrl = baseUrl.toHttpUrl() }
- fun timeout(timeout: Duration) = apply { this.timeout = timeout }
+ fun timeout(timeout: Timeout) = apply { this.timeout = timeout }
+
+ fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build())
fun proxy(proxy: Proxy?) = apply { this.proxy = proxy }
fun build(): OkHttpClient =
OkHttpClient(
okhttp3.OkHttpClient.Builder()
- .connectTimeout(timeout)
- .readTimeout(timeout)
- .writeTimeout(timeout)
- .callTimeout(if (timeout.seconds == 0L) timeout else timeout.plusSeconds(30))
+ .connectTimeout(timeout.connect())
+ .readTimeout(timeout.read())
+ .writeTimeout(timeout.write())
+ .callTimeout(timeout.request())
.proxy(proxy)
.build(),
- checkNotNull(baseUrl) { "`baseUrl` is required but was not set" },
+ checkRequired("baseUrl", baseUrl),
)
}
}
+
+// --- ✅ New class added below ---
+class LoggingInterceptor : Interceptor {
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val request = chain.request()
+ println("➡️ Sending request: ${request.method} ${request.url}")
+ val response = chain.proceed(request)
+ println("⬅️ Received response: ${response.code} ${response.request.url}")
+ return response
+ }
+}
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 4c71daa..1c947e1 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
@@ -9,33 +9,48 @@ import java.time.Duration
import org.onebusaway.client.OnebusawaySdkClient
import org.onebusaway.client.OnebusawaySdkClientImpl
import org.onebusaway.core.ClientOptions
+import org.onebusaway.core.Timeout
+import org.onebusaway.core.http.Headers
+import org.onebusaway.core.http.QueryParams
class OnebusawaySdkOkHttpClient private constructor() {
companion object {
+ /**
+ * Returns a mutable builder for constructing an instance of [OnebusawaySdkOkHttpClient].
+ */
@JvmStatic fun builder() = Builder()
@JvmStatic fun fromEnv(): OnebusawaySdkClient = builder().fromEnv().build()
}
- class Builder {
+ /** A builder for [OnebusawaySdkOkHttpClient]. */
+ class Builder internal constructor() {
private var clientOptions: ClientOptions.Builder = ClientOptions.builder()
- private var baseUrl: String = ClientOptions.PRODUCTION_URL
- // The default timeout for the client is 1 minute.
- private var timeout: Duration = Duration.ofSeconds(60)
+ private var timeout: Timeout = Timeout.default()
private var proxy: Proxy? = null
- fun baseUrl(baseUrl: String) = apply {
- clientOptions.baseUrl(baseUrl)
- this.baseUrl = baseUrl
+ fun baseUrl(baseUrl: String) = apply { clientOptions.baseUrl(baseUrl) }
+
+ /**
+ * Whether to throw an exception if any of the Jackson versions detected at runtime are
+ * incompatible with the SDK's minimum supported Jackson version (2.13.4).
+ *
+ * Defaults to true. Use extreme caution when disabling this option. There is no guarantee
+ * that the SDK will work correctly when using an incompatible Jackson version.
+ */
+ fun checkJacksonVersionCompatibility(checkJacksonVersionCompatibility: Boolean) = apply {
+ clientOptions.checkJacksonVersionCompatibility(checkJacksonVersionCompatibility)
}
fun jsonMapper(jsonMapper: JsonMapper) = apply { clientOptions.jsonMapper(jsonMapper) }
fun clock(clock: Clock) = apply { clientOptions.clock(clock) }
+ fun headers(headers: Headers) = apply { clientOptions.headers(headers) }
+
fun headers(headers: Map>) = apply {
clientOptions.headers(headers)
}
@@ -46,6 +61,8 @@ class OnebusawaySdkOkHttpClient private constructor() {
clientOptions.putHeaders(name, values)
}
+ fun putAllHeaders(headers: Headers) = apply { clientOptions.putAllHeaders(headers) }
+
fun putAllHeaders(headers: Map>) = apply {
clientOptions.putAllHeaders(headers)
}
@@ -58,6 +75,8 @@ class OnebusawaySdkOkHttpClient private constructor() {
clientOptions.replaceHeaders(name, values)
}
+ fun replaceAllHeaders(headers: Headers) = apply { clientOptions.replaceAllHeaders(headers) }
+
fun replaceAllHeaders(headers: Map>) = apply {
clientOptions.replaceAllHeaders(headers)
}
@@ -66,6 +85,8 @@ class OnebusawaySdkOkHttpClient private constructor() {
fun removeAllHeaders(names: Set) = apply { clientOptions.removeAllHeaders(names) }
+ fun queryParams(queryParams: QueryParams) = apply { clientOptions.queryParams(queryParams) }
+
fun queryParams(queryParams: Map>) = apply {
clientOptions.queryParams(queryParams)
}
@@ -78,6 +99,10 @@ class OnebusawaySdkOkHttpClient private constructor() {
clientOptions.putQueryParams(key, values)
}
+ fun putAllQueryParams(queryParams: QueryParams) = apply {
+ clientOptions.putAllQueryParams(queryParams)
+ }
+
fun putAllQueryParams(queryParams: Map>) = apply {
clientOptions.putAllQueryParams(queryParams)
}
@@ -90,6 +115,10 @@ class OnebusawaySdkOkHttpClient private constructor() {
clientOptions.replaceQueryParams(key, values)
}
+ fun replaceAllQueryParams(queryParams: QueryParams) = apply {
+ clientOptions.replaceAllQueryParams(queryParams)
+ }
+
fun replaceAllQueryParams(queryParams: Map>) = apply {
clientOptions.replaceAllQueryParams(queryParams)
}
@@ -100,7 +129,19 @@ class OnebusawaySdkOkHttpClient private constructor() {
clientOptions.removeAllQueryParams(keys)
}
- fun timeout(timeout: Duration) = apply { this.timeout = timeout }
+ fun timeout(timeout: Timeout) = apply {
+ clientOptions.timeout(timeout)
+ this.timeout = timeout
+ }
+
+ /**
+ * Sets the maximum time allowed for a complete HTTP call, not including retries.
+ *
+ * See [Timeout.request] for more details.
+ *
+ * For fine-grained control, pass a [Timeout] object.
+ */
+ fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build())
fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
@@ -114,12 +155,17 @@ class OnebusawaySdkOkHttpClient private constructor() {
fun fromEnv() = apply { clientOptions.fromEnv() }
+ /**
+ * Returns an immutable instance of [OnebusawaySdkClient].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ */
fun build(): OnebusawaySdkClient =
OnebusawaySdkClientImpl(
clientOptions
.httpClient(
OkHttpClient.builder()
- .baseUrl(baseUrl)
+ .baseUrl(clientOptions.baseUrl())
.timeout(timeout)
.proxy(proxy)
.build()
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 00ea3d2..40b84fd 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
@@ -9,33 +9,49 @@ import java.time.Duration
import org.onebusaway.client.OnebusawaySdkClientAsync
import org.onebusaway.client.OnebusawaySdkClientAsyncImpl
import org.onebusaway.core.ClientOptions
+import org.onebusaway.core.Timeout
+import org.onebusaway.core.http.Headers
+import org.onebusaway.core.http.QueryParams
class OnebusawaySdkOkHttpClientAsync private constructor() {
companion object {
+ /**
+ * Returns a mutable builder for constructing an instance of
+ * [OnebusawaySdkOkHttpClientAsync].
+ */
@JvmStatic fun builder() = Builder()
@JvmStatic fun fromEnv(): OnebusawaySdkClientAsync = builder().fromEnv().build()
}
- class Builder {
+ /** A builder for [OnebusawaySdkOkHttpClientAsync]. */
+ class Builder internal constructor() {
private var clientOptions: ClientOptions.Builder = ClientOptions.builder()
- private var baseUrl: String = ClientOptions.PRODUCTION_URL
- // The default timeout for the client is 1 minute.
- private var timeout: Duration = Duration.ofSeconds(60)
+ private var timeout: Timeout = Timeout.default()
private var proxy: Proxy? = null
- fun baseUrl(baseUrl: String) = apply {
- clientOptions.baseUrl(baseUrl)
- this.baseUrl = baseUrl
+ fun baseUrl(baseUrl: String) = apply { clientOptions.baseUrl(baseUrl) }
+
+ /**
+ * Whether to throw an exception if any of the Jackson versions detected at runtime are
+ * incompatible with the SDK's minimum supported Jackson version (2.13.4).
+ *
+ * Defaults to true. Use extreme caution when disabling this option. There is no guarantee
+ * that the SDK will work correctly when using an incompatible Jackson version.
+ */
+ fun checkJacksonVersionCompatibility(checkJacksonVersionCompatibility: Boolean) = apply {
+ clientOptions.checkJacksonVersionCompatibility(checkJacksonVersionCompatibility)
}
fun jsonMapper(jsonMapper: JsonMapper) = apply { clientOptions.jsonMapper(jsonMapper) }
fun clock(clock: Clock) = apply { clientOptions.clock(clock) }
+ fun headers(headers: Headers) = apply { clientOptions.headers(headers) }
+
fun headers(headers: Map>) = apply {
clientOptions.headers(headers)
}
@@ -46,6 +62,8 @@ class OnebusawaySdkOkHttpClientAsync private constructor() {
clientOptions.putHeaders(name, values)
}
+ fun putAllHeaders(headers: Headers) = apply { clientOptions.putAllHeaders(headers) }
+
fun putAllHeaders(headers: Map>) = apply {
clientOptions.putAllHeaders(headers)
}
@@ -58,6 +76,8 @@ class OnebusawaySdkOkHttpClientAsync private constructor() {
clientOptions.replaceHeaders(name, values)
}
+ fun replaceAllHeaders(headers: Headers) = apply { clientOptions.replaceAllHeaders(headers) }
+
fun replaceAllHeaders(headers: Map>) = apply {
clientOptions.replaceAllHeaders(headers)
}
@@ -66,6 +86,8 @@ class OnebusawaySdkOkHttpClientAsync private constructor() {
fun removeAllHeaders(names: Set) = apply { clientOptions.removeAllHeaders(names) }
+ fun queryParams(queryParams: QueryParams) = apply { clientOptions.queryParams(queryParams) }
+
fun queryParams(queryParams: Map>) = apply {
clientOptions.queryParams(queryParams)
}
@@ -78,6 +100,10 @@ class OnebusawaySdkOkHttpClientAsync private constructor() {
clientOptions.putQueryParams(key, values)
}
+ fun putAllQueryParams(queryParams: QueryParams) = apply {
+ clientOptions.putAllQueryParams(queryParams)
+ }
+
fun putAllQueryParams(queryParams: Map>) = apply {
clientOptions.putAllQueryParams(queryParams)
}
@@ -90,6 +116,10 @@ class OnebusawaySdkOkHttpClientAsync private constructor() {
clientOptions.replaceQueryParams(key, values)
}
+ fun replaceAllQueryParams(queryParams: QueryParams) = apply {
+ clientOptions.replaceAllQueryParams(queryParams)
+ }
+
fun replaceAllQueryParams(queryParams: Map>) = apply {
clientOptions.replaceAllQueryParams(queryParams)
}
@@ -100,7 +130,19 @@ class OnebusawaySdkOkHttpClientAsync private constructor() {
clientOptions.removeAllQueryParams(keys)
}
- fun timeout(timeout: Duration) = apply { this.timeout = timeout }
+ fun timeout(timeout: Timeout) = apply {
+ clientOptions.timeout(timeout)
+ this.timeout = timeout
+ }
+
+ /**
+ * Sets the maximum time allowed for a complete HTTP call, not including retries.
+ *
+ * See [Timeout.request] for more details.
+ *
+ * For fine-grained control, pass a [Timeout] object.
+ */
+ fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build())
fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
@@ -114,12 +156,17 @@ class OnebusawaySdkOkHttpClientAsync private constructor() {
fun fromEnv() = apply { clientOptions.fromEnv() }
+ /**
+ * Returns an immutable instance of [OnebusawaySdkClientAsync].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ */
fun build(): OnebusawaySdkClientAsync =
OnebusawaySdkClientAsyncImpl(
clientOptions
.httpClient(
OkHttpClient.builder()
- .baseUrl(baseUrl)
+ .baseUrl(clientOptions.baseUrl())
.timeout(timeout)
.proxy(proxy)
.build()
diff --git a/onebusaway-sdk-java-core/build.gradle.kts b/onebusaway-sdk-java-core/build.gradle.kts
index 8f52193..c020c89 100644
--- a/onebusaway-sdk-java-core/build.gradle.kts
+++ b/onebusaway-sdk-java-core/build.gradle.kts
@@ -3,15 +3,28 @@ plugins {
id("onebusaway-sdk.publish")
}
+configurations.all {
+ resolutionStrategy {
+ // Compile and test against a lower Jackson version to ensure we're compatible with it.
+ // We publish with a higher version (see below) to ensure users depend on a secure version by default.
+ force("com.fasterxml.jackson.core:jackson-core:2.13.4")
+ force("com.fasterxml.jackson.core:jackson-databind:2.13.4")
+ force("com.fasterxml.jackson.core:jackson-annotations:2.13.4")
+ force("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.4")
+ force("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.4")
+ force("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.4")
+ }
+}
+
dependencies {
- api("com.fasterxml.jackson.core:jackson-core:2.14.3")
- api("com.fasterxml.jackson.core:jackson-databind:2.14.3")
- api("com.google.guava:guava:33.0.0-jre")
+ api("com.fasterxml.jackson.core:jackson-core:2.18.2")
+ api("com.fasterxml.jackson.core:jackson-databind:2.18.2")
+ api("com.google.errorprone:error_prone_annotations:2.33.0")
- implementation("com.fasterxml.jackson.core:jackson-annotations:2.14.3")
- implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.14.3")
- implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.14.3")
- implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.3")
+ implementation("com.fasterxml.jackson.core:jackson-annotations:2.18.2")
+ implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.18.2")
+ implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2")
+ implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.18.2")
implementation("org.apache.httpcomponents.core5:httpcore5:5.2.4")
implementation("org.apache.httpcomponents.client5:httpclient5:5.3.1")
@@ -19,8 +32,10 @@ dependencies {
testImplementation(project(":onebusaway-sdk-java-client-okhttp"))
testImplementation("com.github.tomakehurst:wiremock-jre8:2.35.2")
testImplementation("org.assertj:assertj-core:3.25.3")
- testImplementation("org.assertj:assertj-guava:3.25.3")
- testImplementation("org.slf4j:slf4j-simple:2.0.12")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3")
testImplementation("org.junit.jupiter:junit-jupiter-params:5.9.3")
+ testImplementation("org.junit-pioneer:junit-pioneer:1.9.1")
+ testImplementation("org.mockito:mockito-core:5.14.2")
+ testImplementation("org.mockito:mockito-junit-jupiter:5.14.2")
+ testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0")
}
diff --git a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/client/OnebusawaySdkClient.kt b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/client/OnebusawaySdkClient.kt
index 44afaea..6128438 100644
--- a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/client/OnebusawaySdkClient.kt
+++ b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/client/OnebusawaySdkClient.kt
@@ -2,13 +2,64 @@
package org.onebusaway.client
-import org.onebusaway.models.*
-import org.onebusaway.services.blocking.*
-
+import org.onebusaway.services.blocking.AgenciesWithCoverageService
+import org.onebusaway.services.blocking.AgencyService
+import org.onebusaway.services.blocking.ArrivalAndDepartureService
+import org.onebusaway.services.blocking.BlockService
+import org.onebusaway.services.blocking.ConfigService
+import org.onebusaway.services.blocking.CurrentTimeService
+import org.onebusaway.services.blocking.ReportProblemWithStopService
+import org.onebusaway.services.blocking.ReportProblemWithTripService
+import org.onebusaway.services.blocking.RouteIdsForAgencyService
+import org.onebusaway.services.blocking.RouteService
+import org.onebusaway.services.blocking.RoutesForAgencyService
+import org.onebusaway.services.blocking.RoutesForLocationService
+import org.onebusaway.services.blocking.ScheduleForRouteService
+import org.onebusaway.services.blocking.ScheduleForStopService
+import org.onebusaway.services.blocking.SearchForRouteService
+import org.onebusaway.services.blocking.SearchForStopService
+import org.onebusaway.services.blocking.ShapeService
+import org.onebusaway.services.blocking.StopIdsForAgencyService
+import org.onebusaway.services.blocking.StopService
+import org.onebusaway.services.blocking.StopsForAgencyService
+import org.onebusaway.services.blocking.StopsForLocationService
+import org.onebusaway.services.blocking.StopsForRouteService
+import org.onebusaway.services.blocking.TripDetailService
+import org.onebusaway.services.blocking.TripForVehicleService
+import org.onebusaway.services.blocking.TripService
+import org.onebusaway.services.blocking.TripsForLocationService
+import org.onebusaway.services.blocking.TripsForRouteService
+import org.onebusaway.services.blocking.VehiclesForAgencyService
+
+/**
+ * A client for interacting with the Onebusaway SDK REST API synchronously. You can also switch to
+ * asynchronous execution via the [async] method.
+ *
+ * This client performs best when you create a single instance and reuse it for all interactions
+ * with the REST API. This is because each client holds its own connection pool and thread pools.
+ * Reusing connections and threads reduces latency and saves memory. The client also handles rate
+ * limiting per client. This means that creating and using multiple instances at the same time will
+ * not respect rate limits.
+ *
+ * The threads and connections that are held will be released automatically if they remain idle. But
+ * if you are writing an application that needs to aggressively release unused resources, then you
+ * may call [close].
+ */
interface OnebusawaySdkClient {
+ /**
+ * Returns a version of this client that uses asynchronous execution.
+ *
+ * The returned client shares its resources, like its connection pool and thread pools, with
+ * this client.
+ */
fun async(): OnebusawaySdkClientAsync
+ /**
+ * Returns a view of this service that provides access to raw HTTP responses for each method.
+ */
+ fun withRawResponse(): WithRawResponse
+
fun agenciesWithCoverage(): AgenciesWithCoverageService
fun agency(): AgencyService
@@ -64,4 +115,79 @@ interface OnebusawaySdkClient {
fun block(): BlockService
fun shape(): ShapeService
+
+ /**
+ * Closes this client, relinquishing any underlying resources.
+ *
+ * This is purposefully not inherited from [AutoCloseable] because the client is long-lived and
+ * usually should not be synchronously closed via try-with-resources.
+ *
+ * It's also usually not necessary to call this method at all. the default HTTP client
+ * automatically releases threads and connections if they remain idle, but if you are writing an
+ * application that needs to aggressively release unused resources, then you may call this
+ * method.
+ */
+ fun close()
+
+ /**
+ * A view of [OnebusawaySdkClient] that provides access to raw HTTP responses for each method.
+ */
+ interface WithRawResponse {
+
+ fun agenciesWithCoverage(): AgenciesWithCoverageService.WithRawResponse
+
+ fun agency(): AgencyService.WithRawResponse
+
+ fun vehiclesForAgency(): VehiclesForAgencyService.WithRawResponse
+
+ fun config(): ConfigService.WithRawResponse
+
+ fun currentTime(): CurrentTimeService.WithRawResponse
+
+ fun stopsForLocation(): StopsForLocationService.WithRawResponse
+
+ fun stopsForRoute(): StopsForRouteService.WithRawResponse
+
+ fun stopsForAgency(): StopsForAgencyService.WithRawResponse
+
+ fun stop(): StopService.WithRawResponse
+
+ fun stopIdsForAgency(): StopIdsForAgencyService.WithRawResponse
+
+ fun scheduleForStop(): ScheduleForStopService.WithRawResponse
+
+ fun route(): RouteService.WithRawResponse
+
+ fun routeIdsForAgency(): RouteIdsForAgencyService.WithRawResponse
+
+ fun routesForLocation(): RoutesForLocationService.WithRawResponse
+
+ fun routesForAgency(): RoutesForAgencyService.WithRawResponse
+
+ fun scheduleForRoute(): ScheduleForRouteService.WithRawResponse
+
+ fun arrivalAndDeparture(): ArrivalAndDepartureService.WithRawResponse
+
+ fun trip(): TripService.WithRawResponse
+
+ fun tripsForLocation(): TripsForLocationService.WithRawResponse
+
+ fun tripDetails(): TripDetailService.WithRawResponse
+
+ fun tripForVehicle(): TripForVehicleService.WithRawResponse
+
+ fun tripsForRoute(): TripsForRouteService.WithRawResponse
+
+ fun reportProblemWithStop(): ReportProblemWithStopService.WithRawResponse
+
+ fun reportProblemWithTrip(): ReportProblemWithTripService.WithRawResponse
+
+ fun searchForStop(): SearchForStopService.WithRawResponse
+
+ fun searchForRoute(): SearchForRouteService.WithRawResponse
+
+ fun block(): BlockService.WithRawResponse
+
+ fun shape(): ShapeService.WithRawResponse
+ }
}
diff --git a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/client/OnebusawaySdkClientAsync.kt b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/client/OnebusawaySdkClientAsync.kt
index 0029e0f..df8c27f 100644
--- a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/client/OnebusawaySdkClientAsync.kt
+++ b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/client/OnebusawaySdkClientAsync.kt
@@ -2,13 +2,64 @@
package org.onebusaway.client
-import org.onebusaway.models.*
-import org.onebusaway.services.async.*
-
+import org.onebusaway.services.async.AgenciesWithCoverageServiceAsync
+import org.onebusaway.services.async.AgencyServiceAsync
+import org.onebusaway.services.async.ArrivalAndDepartureServiceAsync
+import org.onebusaway.services.async.BlockServiceAsync
+import org.onebusaway.services.async.ConfigServiceAsync
+import org.onebusaway.services.async.CurrentTimeServiceAsync
+import org.onebusaway.services.async.ReportProblemWithStopServiceAsync
+import org.onebusaway.services.async.ReportProblemWithTripServiceAsync
+import org.onebusaway.services.async.RouteIdsForAgencyServiceAsync
+import org.onebusaway.services.async.RouteServiceAsync
+import org.onebusaway.services.async.RoutesForAgencyServiceAsync
+import org.onebusaway.services.async.RoutesForLocationServiceAsync
+import org.onebusaway.services.async.ScheduleForRouteServiceAsync
+import org.onebusaway.services.async.ScheduleForStopServiceAsync
+import org.onebusaway.services.async.SearchForRouteServiceAsync
+import org.onebusaway.services.async.SearchForStopServiceAsync
+import org.onebusaway.services.async.ShapeServiceAsync
+import org.onebusaway.services.async.StopIdsForAgencyServiceAsync
+import org.onebusaway.services.async.StopServiceAsync
+import org.onebusaway.services.async.StopsForAgencyServiceAsync
+import org.onebusaway.services.async.StopsForLocationServiceAsync
+import org.onebusaway.services.async.StopsForRouteServiceAsync
+import org.onebusaway.services.async.TripDetailServiceAsync
+import org.onebusaway.services.async.TripForVehicleServiceAsync
+import org.onebusaway.services.async.TripServiceAsync
+import org.onebusaway.services.async.TripsForLocationServiceAsync
+import org.onebusaway.services.async.TripsForRouteServiceAsync
+import org.onebusaway.services.async.VehiclesForAgencyServiceAsync
+
+/**
+ * A client for interacting with the Onebusaway SDK REST API asynchronously. You can also switch to
+ * synchronous execution via the [sync] method.
+ *
+ * This client performs best when you create a single instance and reuse it for all interactions
+ * with the REST API. This is because each client holds its own connection pool and thread pools.
+ * Reusing connections and threads reduces latency and saves memory. The client also handles rate
+ * limiting per client. This means that creating and using multiple instances at the same time will
+ * not respect rate limits.
+ *
+ * The threads and connections that are held will be released automatically if they remain idle. But
+ * if you are writing an application that needs to aggressively release unused resources, then you
+ * may call [close].
+ */
interface OnebusawaySdkClientAsync {
+ /**
+ * Returns a version of this client that uses synchronous execution.
+ *
+ * The returned client shares its resources, like its connection pool and thread pools, with
+ * this client.
+ */
fun sync(): OnebusawaySdkClient
+ /**
+ * Returns a view of this service that provides access to raw HTTP responses for each method.
+ */
+ fun withRawResponse(): WithRawResponse
+
fun agenciesWithCoverage(): AgenciesWithCoverageServiceAsync
fun agency(): AgencyServiceAsync
@@ -64,4 +115,80 @@ interface OnebusawaySdkClientAsync {
fun block(): BlockServiceAsync
fun shape(): ShapeServiceAsync
+
+ /**
+ * Closes this client, relinquishing any underlying resources.
+ *
+ * This is purposefully not inherited from [AutoCloseable] because the client is long-lived and
+ * usually should not be synchronously closed via try-with-resources.
+ *
+ * It's also usually not necessary to call this method at all. the default HTTP client
+ * automatically releases threads and connections if they remain idle, but if you are writing an
+ * application that needs to aggressively release unused resources, then you may call this
+ * method.
+ */
+ fun close()
+
+ /**
+ * A view of [OnebusawaySdkClientAsync] that provides access to raw HTTP responses for each
+ * method.
+ */
+ interface WithRawResponse {
+
+ fun agenciesWithCoverage(): AgenciesWithCoverageServiceAsync.WithRawResponse
+
+ fun agency(): AgencyServiceAsync.WithRawResponse
+
+ fun vehiclesForAgency(): VehiclesForAgencyServiceAsync.WithRawResponse
+
+ fun config(): ConfigServiceAsync.WithRawResponse
+
+ fun currentTime(): CurrentTimeServiceAsync.WithRawResponse
+
+ fun stopsForLocation(): StopsForLocationServiceAsync.WithRawResponse
+
+ fun stopsForRoute(): StopsForRouteServiceAsync.WithRawResponse
+
+ fun stopsForAgency(): StopsForAgencyServiceAsync.WithRawResponse
+
+ fun stop(): StopServiceAsync.WithRawResponse
+
+ fun stopIdsForAgency(): StopIdsForAgencyServiceAsync.WithRawResponse
+
+ fun scheduleForStop(): ScheduleForStopServiceAsync.WithRawResponse
+
+ fun route(): RouteServiceAsync.WithRawResponse
+
+ fun routeIdsForAgency(): RouteIdsForAgencyServiceAsync.WithRawResponse
+
+ fun routesForLocation(): RoutesForLocationServiceAsync.WithRawResponse
+
+ fun routesForAgency(): RoutesForAgencyServiceAsync.WithRawResponse
+
+ fun scheduleForRoute(): ScheduleForRouteServiceAsync.WithRawResponse
+
+ fun arrivalAndDeparture(): ArrivalAndDepartureServiceAsync.WithRawResponse
+
+ fun trip(): TripServiceAsync.WithRawResponse
+
+ fun tripsForLocation(): TripsForLocationServiceAsync.WithRawResponse
+
+ fun tripDetails(): TripDetailServiceAsync.WithRawResponse
+
+ fun tripForVehicle(): TripForVehicleServiceAsync.WithRawResponse
+
+ fun tripsForRoute(): TripsForRouteServiceAsync.WithRawResponse
+
+ fun reportProblemWithStop(): ReportProblemWithStopServiceAsync.WithRawResponse
+
+ fun reportProblemWithTrip(): ReportProblemWithTripServiceAsync.WithRawResponse
+
+ fun searchForStop(): SearchForStopServiceAsync.WithRawResponse
+
+ fun searchForRoute(): SearchForRouteServiceAsync.WithRawResponse
+
+ fun block(): BlockServiceAsync.WithRawResponse
+
+ fun shape(): ShapeServiceAsync.WithRawResponse
+ }
}
diff --git a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/client/OnebusawaySdkClientAsyncImpl.kt b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/client/OnebusawaySdkClientAsyncImpl.kt
index f930dc9..b03336a 100644
--- a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/client/OnebusawaySdkClientAsyncImpl.kt
+++ b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/client/OnebusawaySdkClientAsyncImpl.kt
@@ -4,16 +4,68 @@ package org.onebusaway.client
import org.onebusaway.core.ClientOptions
import org.onebusaway.core.getPackageVersion
-import org.onebusaway.models.*
-import org.onebusaway.services.async.*
-
-class OnebusawaySdkClientAsyncImpl
-constructor(
- private val clientOptions: ClientOptions,
-) : OnebusawaySdkClientAsync {
+import org.onebusaway.services.async.AgenciesWithCoverageServiceAsync
+import org.onebusaway.services.async.AgenciesWithCoverageServiceAsyncImpl
+import org.onebusaway.services.async.AgencyServiceAsync
+import org.onebusaway.services.async.AgencyServiceAsyncImpl
+import org.onebusaway.services.async.ArrivalAndDepartureServiceAsync
+import org.onebusaway.services.async.ArrivalAndDepartureServiceAsyncImpl
+import org.onebusaway.services.async.BlockServiceAsync
+import org.onebusaway.services.async.BlockServiceAsyncImpl
+import org.onebusaway.services.async.ConfigServiceAsync
+import org.onebusaway.services.async.ConfigServiceAsyncImpl
+import org.onebusaway.services.async.CurrentTimeServiceAsync
+import org.onebusaway.services.async.CurrentTimeServiceAsyncImpl
+import org.onebusaway.services.async.ReportProblemWithStopServiceAsync
+import org.onebusaway.services.async.ReportProblemWithStopServiceAsyncImpl
+import org.onebusaway.services.async.ReportProblemWithTripServiceAsync
+import org.onebusaway.services.async.ReportProblemWithTripServiceAsyncImpl
+import org.onebusaway.services.async.RouteIdsForAgencyServiceAsync
+import org.onebusaway.services.async.RouteIdsForAgencyServiceAsyncImpl
+import org.onebusaway.services.async.RouteServiceAsync
+import org.onebusaway.services.async.RouteServiceAsyncImpl
+import org.onebusaway.services.async.RoutesForAgencyServiceAsync
+import org.onebusaway.services.async.RoutesForAgencyServiceAsyncImpl
+import org.onebusaway.services.async.RoutesForLocationServiceAsync
+import org.onebusaway.services.async.RoutesForLocationServiceAsyncImpl
+import org.onebusaway.services.async.ScheduleForRouteServiceAsync
+import org.onebusaway.services.async.ScheduleForRouteServiceAsyncImpl
+import org.onebusaway.services.async.ScheduleForStopServiceAsync
+import org.onebusaway.services.async.ScheduleForStopServiceAsyncImpl
+import org.onebusaway.services.async.SearchForRouteServiceAsync
+import org.onebusaway.services.async.SearchForRouteServiceAsyncImpl
+import org.onebusaway.services.async.SearchForStopServiceAsync
+import org.onebusaway.services.async.SearchForStopServiceAsyncImpl
+import org.onebusaway.services.async.ShapeServiceAsync
+import org.onebusaway.services.async.ShapeServiceAsyncImpl
+import org.onebusaway.services.async.StopIdsForAgencyServiceAsync
+import org.onebusaway.services.async.StopIdsForAgencyServiceAsyncImpl
+import org.onebusaway.services.async.StopServiceAsync
+import org.onebusaway.services.async.StopServiceAsyncImpl
+import org.onebusaway.services.async.StopsForAgencyServiceAsync
+import org.onebusaway.services.async.StopsForAgencyServiceAsyncImpl
+import org.onebusaway.services.async.StopsForLocationServiceAsync
+import org.onebusaway.services.async.StopsForLocationServiceAsyncImpl
+import org.onebusaway.services.async.StopsForRouteServiceAsync
+import org.onebusaway.services.async.StopsForRouteServiceAsyncImpl
+import org.onebusaway.services.async.TripDetailServiceAsync
+import org.onebusaway.services.async.TripDetailServiceAsyncImpl
+import org.onebusaway.services.async.TripForVehicleServiceAsync
+import org.onebusaway.services.async.TripForVehicleServiceAsyncImpl
+import org.onebusaway.services.async.TripServiceAsync
+import org.onebusaway.services.async.TripServiceAsyncImpl
+import org.onebusaway.services.async.TripsForLocationServiceAsync
+import org.onebusaway.services.async.TripsForLocationServiceAsyncImpl
+import org.onebusaway.services.async.TripsForRouteServiceAsync
+import org.onebusaway.services.async.TripsForRouteServiceAsyncImpl
+import org.onebusaway.services.async.VehiclesForAgencyServiceAsync
+import org.onebusaway.services.async.VehiclesForAgencyServiceAsyncImpl
+
+class OnebusawaySdkClientAsyncImpl(private val clientOptions: ClientOptions) :
+ OnebusawaySdkClientAsync {
private val clientOptionsWithUserAgent =
- if (clientOptions.headers.containsKey("User-Agent")) clientOptions
+ if (clientOptions.headers.names().contains("User-Agent")) clientOptions
else
clientOptions
.toBuilder()
@@ -23,6 +75,10 @@ constructor(
// Pass the original clientOptions so that this client sets its own User-Agent.
private val sync: OnebusawaySdkClient by lazy { OnebusawaySdkClientImpl(clientOptions) }
+ private val withRawResponse: OnebusawaySdkClientAsync.WithRawResponse by lazy {
+ WithRawResponseImpl(clientOptions)
+ }
+
private val agenciesWithCoverage: AgenciesWithCoverageServiceAsync by lazy {
AgenciesWithCoverageServiceAsyncImpl(clientOptionsWithUserAgent)
}
@@ -133,6 +189,8 @@ constructor(
override fun sync(): OnebusawaySdkClient = sync
+ override fun withRawResponse(): OnebusawaySdkClientAsync.WithRawResponse = withRawResponse
+
override fun agenciesWithCoverage(): AgenciesWithCoverageServiceAsync = agenciesWithCoverage
override fun agency(): AgencyServiceAsync = agency
@@ -188,4 +246,193 @@ constructor(
override fun block(): BlockServiceAsync = block
override fun shape(): ShapeServiceAsync = shape
+
+ override fun close() = clientOptions.httpClient.close()
+
+ class WithRawResponseImpl internal constructor(private val clientOptions: ClientOptions) :
+ OnebusawaySdkClientAsync.WithRawResponse {
+
+ private val agenciesWithCoverage: AgenciesWithCoverageServiceAsync.WithRawResponse by lazy {
+ AgenciesWithCoverageServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val agency: AgencyServiceAsync.WithRawResponse by lazy {
+ AgencyServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val vehiclesForAgency: VehiclesForAgencyServiceAsync.WithRawResponse by lazy {
+ VehiclesForAgencyServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val config: ConfigServiceAsync.WithRawResponse by lazy {
+ ConfigServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val currentTime: CurrentTimeServiceAsync.WithRawResponse by lazy {
+ CurrentTimeServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val stopsForLocation: StopsForLocationServiceAsync.WithRawResponse by lazy {
+ StopsForLocationServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val stopsForRoute: StopsForRouteServiceAsync.WithRawResponse by lazy {
+ StopsForRouteServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val stopsForAgency: StopsForAgencyServiceAsync.WithRawResponse by lazy {
+ StopsForAgencyServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val stop: StopServiceAsync.WithRawResponse by lazy {
+ StopServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val stopIdsForAgency: StopIdsForAgencyServiceAsync.WithRawResponse by lazy {
+ StopIdsForAgencyServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val scheduleForStop: ScheduleForStopServiceAsync.WithRawResponse by lazy {
+ ScheduleForStopServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val route: RouteServiceAsync.WithRawResponse by lazy {
+ RouteServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val routeIdsForAgency: RouteIdsForAgencyServiceAsync.WithRawResponse by lazy {
+ RouteIdsForAgencyServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val routesForLocation: RoutesForLocationServiceAsync.WithRawResponse by lazy {
+ RoutesForLocationServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val routesForAgency: RoutesForAgencyServiceAsync.WithRawResponse by lazy {
+ RoutesForAgencyServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val scheduleForRoute: ScheduleForRouteServiceAsync.WithRawResponse by lazy {
+ ScheduleForRouteServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val arrivalAndDeparture: ArrivalAndDepartureServiceAsync.WithRawResponse by lazy {
+ ArrivalAndDepartureServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val trip: TripServiceAsync.WithRawResponse by lazy {
+ TripServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val tripsForLocation: TripsForLocationServiceAsync.WithRawResponse by lazy {
+ TripsForLocationServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val tripDetails: TripDetailServiceAsync.WithRawResponse by lazy {
+ TripDetailServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val tripForVehicle: TripForVehicleServiceAsync.WithRawResponse by lazy {
+ TripForVehicleServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val tripsForRoute: TripsForRouteServiceAsync.WithRawResponse by lazy {
+ TripsForRouteServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val reportProblemWithStop:
+ ReportProblemWithStopServiceAsync.WithRawResponse by lazy {
+ ReportProblemWithStopServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val reportProblemWithTrip:
+ ReportProblemWithTripServiceAsync.WithRawResponse by lazy {
+ ReportProblemWithTripServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val searchForStop: SearchForStopServiceAsync.WithRawResponse by lazy {
+ SearchForStopServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val searchForRoute: SearchForRouteServiceAsync.WithRawResponse by lazy {
+ SearchForRouteServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val block: BlockServiceAsync.WithRawResponse by lazy {
+ BlockServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val shape: ShapeServiceAsync.WithRawResponse by lazy {
+ ShapeServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ override fun agenciesWithCoverage(): AgenciesWithCoverageServiceAsync.WithRawResponse =
+ agenciesWithCoverage
+
+ override fun agency(): AgencyServiceAsync.WithRawResponse = agency
+
+ override fun vehiclesForAgency(): VehiclesForAgencyServiceAsync.WithRawResponse =
+ vehiclesForAgency
+
+ override fun config(): ConfigServiceAsync.WithRawResponse = config
+
+ override fun currentTime(): CurrentTimeServiceAsync.WithRawResponse = currentTime
+
+ override fun stopsForLocation(): StopsForLocationServiceAsync.WithRawResponse =
+ stopsForLocation
+
+ override fun stopsForRoute(): StopsForRouteServiceAsync.WithRawResponse = stopsForRoute
+
+ override fun stopsForAgency(): StopsForAgencyServiceAsync.WithRawResponse = stopsForAgency
+
+ override fun stop(): StopServiceAsync.WithRawResponse = stop
+
+ override fun stopIdsForAgency(): StopIdsForAgencyServiceAsync.WithRawResponse =
+ stopIdsForAgency
+
+ override fun scheduleForStop(): ScheduleForStopServiceAsync.WithRawResponse =
+ scheduleForStop
+
+ override fun route(): RouteServiceAsync.WithRawResponse = route
+
+ override fun routeIdsForAgency(): RouteIdsForAgencyServiceAsync.WithRawResponse =
+ routeIdsForAgency
+
+ override fun routesForLocation(): RoutesForLocationServiceAsync.WithRawResponse =
+ routesForLocation
+
+ override fun routesForAgency(): RoutesForAgencyServiceAsync.WithRawResponse =
+ routesForAgency
+
+ override fun scheduleForRoute(): ScheduleForRouteServiceAsync.WithRawResponse =
+ scheduleForRoute
+
+ override fun arrivalAndDeparture(): ArrivalAndDepartureServiceAsync.WithRawResponse =
+ arrivalAndDeparture
+
+ override fun trip(): TripServiceAsync.WithRawResponse = trip
+
+ override fun tripsForLocation(): TripsForLocationServiceAsync.WithRawResponse =
+ tripsForLocation
+
+ override fun tripDetails(): TripDetailServiceAsync.WithRawResponse = tripDetails
+
+ override fun tripForVehicle(): TripForVehicleServiceAsync.WithRawResponse = tripForVehicle
+
+ override fun tripsForRoute(): TripsForRouteServiceAsync.WithRawResponse = tripsForRoute
+
+ override fun reportProblemWithStop(): ReportProblemWithStopServiceAsync.WithRawResponse =
+ reportProblemWithStop
+
+ override fun reportProblemWithTrip(): ReportProblemWithTripServiceAsync.WithRawResponse =
+ reportProblemWithTrip
+
+ override fun searchForStop(): SearchForStopServiceAsync.WithRawResponse = searchForStop
+
+ override fun searchForRoute(): SearchForRouteServiceAsync.WithRawResponse = searchForRoute
+
+ override fun block(): BlockServiceAsync.WithRawResponse = block
+
+ override fun shape(): ShapeServiceAsync.WithRawResponse = shape
+ }
}
diff --git a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/client/OnebusawaySdkClientImpl.kt b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/client/OnebusawaySdkClientImpl.kt
index 1183849..b3213c5 100644
--- a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/client/OnebusawaySdkClientImpl.kt
+++ b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/client/OnebusawaySdkClientImpl.kt
@@ -4,16 +4,67 @@ package org.onebusaway.client
import org.onebusaway.core.ClientOptions
import org.onebusaway.core.getPackageVersion
-import org.onebusaway.models.*
-import org.onebusaway.services.blocking.*
-
-class OnebusawaySdkClientImpl
-constructor(
- private val clientOptions: ClientOptions,
-) : OnebusawaySdkClient {
+import org.onebusaway.services.blocking.AgenciesWithCoverageService
+import org.onebusaway.services.blocking.AgenciesWithCoverageServiceImpl
+import org.onebusaway.services.blocking.AgencyService
+import org.onebusaway.services.blocking.AgencyServiceImpl
+import org.onebusaway.services.blocking.ArrivalAndDepartureService
+import org.onebusaway.services.blocking.ArrivalAndDepartureServiceImpl
+import org.onebusaway.services.blocking.BlockService
+import org.onebusaway.services.blocking.BlockServiceImpl
+import org.onebusaway.services.blocking.ConfigService
+import org.onebusaway.services.blocking.ConfigServiceImpl
+import org.onebusaway.services.blocking.CurrentTimeService
+import org.onebusaway.services.blocking.CurrentTimeServiceImpl
+import org.onebusaway.services.blocking.ReportProblemWithStopService
+import org.onebusaway.services.blocking.ReportProblemWithStopServiceImpl
+import org.onebusaway.services.blocking.ReportProblemWithTripService
+import org.onebusaway.services.blocking.ReportProblemWithTripServiceImpl
+import org.onebusaway.services.blocking.RouteIdsForAgencyService
+import org.onebusaway.services.blocking.RouteIdsForAgencyServiceImpl
+import org.onebusaway.services.blocking.RouteService
+import org.onebusaway.services.blocking.RouteServiceImpl
+import org.onebusaway.services.blocking.RoutesForAgencyService
+import org.onebusaway.services.blocking.RoutesForAgencyServiceImpl
+import org.onebusaway.services.blocking.RoutesForLocationService
+import org.onebusaway.services.blocking.RoutesForLocationServiceImpl
+import org.onebusaway.services.blocking.ScheduleForRouteService
+import org.onebusaway.services.blocking.ScheduleForRouteServiceImpl
+import org.onebusaway.services.blocking.ScheduleForStopService
+import org.onebusaway.services.blocking.ScheduleForStopServiceImpl
+import org.onebusaway.services.blocking.SearchForRouteService
+import org.onebusaway.services.blocking.SearchForRouteServiceImpl
+import org.onebusaway.services.blocking.SearchForStopService
+import org.onebusaway.services.blocking.SearchForStopServiceImpl
+import org.onebusaway.services.blocking.ShapeService
+import org.onebusaway.services.blocking.ShapeServiceImpl
+import org.onebusaway.services.blocking.StopIdsForAgencyService
+import org.onebusaway.services.blocking.StopIdsForAgencyServiceImpl
+import org.onebusaway.services.blocking.StopService
+import org.onebusaway.services.blocking.StopServiceImpl
+import org.onebusaway.services.blocking.StopsForAgencyService
+import org.onebusaway.services.blocking.StopsForAgencyServiceImpl
+import org.onebusaway.services.blocking.StopsForLocationService
+import org.onebusaway.services.blocking.StopsForLocationServiceImpl
+import org.onebusaway.services.blocking.StopsForRouteService
+import org.onebusaway.services.blocking.StopsForRouteServiceImpl
+import org.onebusaway.services.blocking.TripDetailService
+import org.onebusaway.services.blocking.TripDetailServiceImpl
+import org.onebusaway.services.blocking.TripForVehicleService
+import org.onebusaway.services.blocking.TripForVehicleServiceImpl
+import org.onebusaway.services.blocking.TripService
+import org.onebusaway.services.blocking.TripServiceImpl
+import org.onebusaway.services.blocking.TripsForLocationService
+import org.onebusaway.services.blocking.TripsForLocationServiceImpl
+import org.onebusaway.services.blocking.TripsForRouteService
+import org.onebusaway.services.blocking.TripsForRouteServiceImpl
+import org.onebusaway.services.blocking.VehiclesForAgencyService
+import org.onebusaway.services.blocking.VehiclesForAgencyServiceImpl
+
+class OnebusawaySdkClientImpl(private val clientOptions: ClientOptions) : OnebusawaySdkClient {
private val clientOptionsWithUserAgent =
- if (clientOptions.headers.containsKey("User-Agent")) clientOptions
+ if (clientOptions.headers.names().contains("User-Agent")) clientOptions
else
clientOptions
.toBuilder()
@@ -25,6 +76,10 @@ constructor(
OnebusawaySdkClientAsyncImpl(clientOptions)
}
+ private val withRawResponse: OnebusawaySdkClient.WithRawResponse by lazy {
+ WithRawResponseImpl(clientOptions)
+ }
+
private val agenciesWithCoverage: AgenciesWithCoverageService by lazy {
AgenciesWithCoverageServiceImpl(clientOptionsWithUserAgent)
}
@@ -125,6 +180,8 @@ constructor(
override fun async(): OnebusawaySdkClientAsync = async
+ override fun withRawResponse(): OnebusawaySdkClient.WithRawResponse = withRawResponse
+
override fun agenciesWithCoverage(): AgenciesWithCoverageService = agenciesWithCoverage
override fun agency(): AgencyService = agency
@@ -180,4 +237,185 @@ constructor(
override fun block(): BlockService = block
override fun shape(): ShapeService = shape
+
+ override fun close() = clientOptions.httpClient.close()
+
+ class WithRawResponseImpl internal constructor(private val clientOptions: ClientOptions) :
+ OnebusawaySdkClient.WithRawResponse {
+
+ private val agenciesWithCoverage: AgenciesWithCoverageService.WithRawResponse by lazy {
+ AgenciesWithCoverageServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val agency: AgencyService.WithRawResponse by lazy {
+ AgencyServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val vehiclesForAgency: VehiclesForAgencyService.WithRawResponse by lazy {
+ VehiclesForAgencyServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val config: ConfigService.WithRawResponse by lazy {
+ ConfigServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val currentTime: CurrentTimeService.WithRawResponse by lazy {
+ CurrentTimeServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val stopsForLocation: StopsForLocationService.WithRawResponse by lazy {
+ StopsForLocationServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val stopsForRoute: StopsForRouteService.WithRawResponse by lazy {
+ StopsForRouteServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val stopsForAgency: StopsForAgencyService.WithRawResponse by lazy {
+ StopsForAgencyServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val stop: StopService.WithRawResponse by lazy {
+ StopServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val stopIdsForAgency: StopIdsForAgencyService.WithRawResponse by lazy {
+ StopIdsForAgencyServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val scheduleForStop: ScheduleForStopService.WithRawResponse by lazy {
+ ScheduleForStopServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val route: RouteService.WithRawResponse by lazy {
+ RouteServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val routeIdsForAgency: RouteIdsForAgencyService.WithRawResponse by lazy {
+ RouteIdsForAgencyServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val routesForLocation: RoutesForLocationService.WithRawResponse by lazy {
+ RoutesForLocationServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val routesForAgency: RoutesForAgencyService.WithRawResponse by lazy {
+ RoutesForAgencyServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val scheduleForRoute: ScheduleForRouteService.WithRawResponse by lazy {
+ ScheduleForRouteServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val arrivalAndDeparture: ArrivalAndDepartureService.WithRawResponse by lazy {
+ ArrivalAndDepartureServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val trip: TripService.WithRawResponse by lazy {
+ TripServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val tripsForLocation: TripsForLocationService.WithRawResponse by lazy {
+ TripsForLocationServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val tripDetails: TripDetailService.WithRawResponse by lazy {
+ TripDetailServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val tripForVehicle: TripForVehicleService.WithRawResponse by lazy {
+ TripForVehicleServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val tripsForRoute: TripsForRouteService.WithRawResponse by lazy {
+ TripsForRouteServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val reportProblemWithStop: ReportProblemWithStopService.WithRawResponse by lazy {
+ ReportProblemWithStopServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val reportProblemWithTrip: ReportProblemWithTripService.WithRawResponse by lazy {
+ ReportProblemWithTripServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val searchForStop: SearchForStopService.WithRawResponse by lazy {
+ SearchForStopServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val searchForRoute: SearchForRouteService.WithRawResponse by lazy {
+ SearchForRouteServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val block: BlockService.WithRawResponse by lazy {
+ BlockServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val shape: ShapeService.WithRawResponse by lazy {
+ ShapeServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ override fun agenciesWithCoverage(): AgenciesWithCoverageService.WithRawResponse =
+ agenciesWithCoverage
+
+ override fun agency(): AgencyService.WithRawResponse = agency
+
+ override fun vehiclesForAgency(): VehiclesForAgencyService.WithRawResponse =
+ vehiclesForAgency
+
+ override fun config(): ConfigService.WithRawResponse = config
+
+ override fun currentTime(): CurrentTimeService.WithRawResponse = currentTime
+
+ override fun stopsForLocation(): StopsForLocationService.WithRawResponse = stopsForLocation
+
+ override fun stopsForRoute(): StopsForRouteService.WithRawResponse = stopsForRoute
+
+ override fun stopsForAgency(): StopsForAgencyService.WithRawResponse = stopsForAgency
+
+ override fun stop(): StopService.WithRawResponse = stop
+
+ override fun stopIdsForAgency(): StopIdsForAgencyService.WithRawResponse = stopIdsForAgency
+
+ override fun scheduleForStop(): ScheduleForStopService.WithRawResponse = scheduleForStop
+
+ override fun route(): RouteService.WithRawResponse = route
+
+ override fun routeIdsForAgency(): RouteIdsForAgencyService.WithRawResponse =
+ routeIdsForAgency
+
+ override fun routesForLocation(): RoutesForLocationService.WithRawResponse =
+ routesForLocation
+
+ override fun routesForAgency(): RoutesForAgencyService.WithRawResponse = routesForAgency
+
+ override fun scheduleForRoute(): ScheduleForRouteService.WithRawResponse = scheduleForRoute
+
+ override fun arrivalAndDeparture(): ArrivalAndDepartureService.WithRawResponse =
+ arrivalAndDeparture
+
+ override fun trip(): TripService.WithRawResponse = trip
+
+ override fun tripsForLocation(): TripsForLocationService.WithRawResponse = tripsForLocation
+
+ override fun tripDetails(): TripDetailService.WithRawResponse = tripDetails
+
+ override fun tripForVehicle(): TripForVehicleService.WithRawResponse = tripForVehicle
+
+ override fun tripsForRoute(): TripsForRouteService.WithRawResponse = tripsForRoute
+
+ override fun reportProblemWithStop(): ReportProblemWithStopService.WithRawResponse =
+ reportProblemWithStop
+
+ override fun reportProblemWithTrip(): ReportProblemWithTripService.WithRawResponse =
+ reportProblemWithTrip
+
+ override fun searchForStop(): SearchForStopService.WithRawResponse = searchForStop
+
+ override fun searchForRoute(): SearchForRouteService.WithRawResponse = searchForRoute
+
+ override fun block(): BlockService.WithRawResponse = block
+
+ override fun shape(): ShapeService.WithRawResponse = shape
+ }
}
diff --git a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/BaseDeserializer.kt b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/BaseDeserializer.kt
index 9602a26..e2902a3 100644
--- a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/BaseDeserializer.kt
+++ b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/BaseDeserializer.kt
@@ -7,7 +7,6 @@ import com.fasterxml.jackson.databind.BeanProperty
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.JsonDeserializer
-import com.fasterxml.jackson.databind.JsonMappingException
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.deser.ContextualDeserializer
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
@@ -18,7 +17,7 @@ abstract class BaseDeserializer(type: KClass) :
override fun createContextual(
context: DeserializationContext,
- property: BeanProperty?
+ property: BeanProperty?,
): JsonDeserializer {
return this
}
@@ -29,31 +28,17 @@ abstract class BaseDeserializer(type: KClass) :
protected abstract fun ObjectCodec.deserialize(node: JsonNode): T
- protected fun ObjectCodec.tryDeserialize(
- node: JsonNode,
- type: TypeReference,
- validate: (T) -> Unit = {}
- ): T? {
- return try {
- readValue(treeAsTokens(node), type).apply(validate)
- } catch (e: JsonMappingException) {
- null
- } catch (e: RuntimeException) {
+ protected fun ObjectCodec.tryDeserialize(node: JsonNode, type: TypeReference): T? =
+ try {
+ readValue(treeAsTokens(node), type)
+ } catch (e: Exception) {
null
}
- }
- protected fun ObjectCodec.tryDeserialize(
- node: JsonNode,
- type: JavaType,
- validate: (T) -> Unit = {}
- ): T? {
- return try {
- readValue(treeAsTokens(node), type).apply(validate)
- } catch (e: JsonMappingException) {
- null
- } catch (e: RuntimeException) {
+ protected fun ObjectCodec.tryDeserialize(node: JsonNode, type: JavaType): T? =
+ try {
+ readValue(treeAsTokens(node), type)
+ } catch (e: Exception) {
null
}
- }
}
diff --git a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/Check.kt b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/Check.kt
new file mode 100644
index 0000000..8ec084c
--- /dev/null
+++ b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/Check.kt
@@ -0,0 +1,96 @@
+@file:JvmName("Check")
+
+package org.onebusaway.core
+
+import com.fasterxml.jackson.core.Version
+import com.fasterxml.jackson.core.util.VersionUtil
+
+fun checkRequired(name: String, condition: Boolean) =
+ check(condition) { "`$name` is required, but was not set" }
+
+fun checkRequired(name: String, value: T?): T =
+ checkNotNull(value) { "`$name` is required, but was not set" }
+
+@JvmSynthetic
+internal fun checkKnown(name: String, value: JsonField): T =
+ value.asKnown().orElseThrow {
+ IllegalStateException("`$name` is not a known type: ${value.javaClass.simpleName}")
+ }
+
+@JvmSynthetic
+internal fun checkKnown(name: String, value: MultipartField): T =
+ value.value.asKnown().orElseThrow {
+ IllegalStateException("`$name` is not a known type: ${value.javaClass.simpleName}")
+ }
+
+@JvmSynthetic
+internal fun checkLength(name: String, value: String, length: Int): String =
+ value.also {
+ check(it.length == length) { "`$name` must have length $length, but was ${it.length}" }
+ }
+
+@JvmSynthetic
+internal fun checkMinLength(name: String, value: String, minLength: Int): String =
+ value.also {
+ check(it.length >= minLength) {
+ if (minLength == 1) "`$name` must be non-empty, but was empty"
+ else "`$name` must have at least length $minLength, but was ${it.length}"
+ }
+ }
+
+@JvmSynthetic
+internal fun checkMaxLength(name: String, value: String, maxLength: Int): String =
+ value.also {
+ check(it.length <= maxLength) {
+ "`$name` must have at most length $maxLength, but was ${it.length}"
+ }
+ }
+
+@JvmSynthetic
+internal fun checkJacksonVersionCompatibility() {
+ val incompatibleJacksonVersions =
+ RUNTIME_JACKSON_VERSIONS.mapNotNull {
+ val badVersionReason = BAD_JACKSON_VERSIONS[it.toString()]
+ when {
+ it.majorVersion != MINIMUM_JACKSON_VERSION.majorVersion ->
+ it to "incompatible major version"
+ it.minorVersion < MINIMUM_JACKSON_VERSION.minorVersion ->
+ it to "minor version too low"
+ it.minorVersion == MINIMUM_JACKSON_VERSION.minorVersion &&
+ it.patchLevel < MINIMUM_JACKSON_VERSION.patchLevel ->
+ it to "patch version too low"
+ badVersionReason != null -> it to badVersionReason
+ else -> null
+ }
+ }
+ check(incompatibleJacksonVersions.isEmpty()) {
+ """
+This SDK requires a minimum Jackson version of $MINIMUM_JACKSON_VERSION, but the following incompatible Jackson versions were detected at runtime:
+
+${incompatibleJacksonVersions.asSequence().map { (version, incompatibilityReason) ->
+ "- `${version.toFullString().replace("/", ":")}` ($incompatibilityReason)"
+}.joinToString("\n")}
+
+This can happen if you are either:
+1. Directly depending on different Jackson versions
+2. Depending on some library that depends on different Jackson versions, potentially transitively
+
+Double-check that you are depending on compatible Jackson versions.
+
+See https://www.github.com/OneBusAway/java-sdk#jackson for more information.
+ """
+ .trimIndent()
+ }
+}
+
+private val MINIMUM_JACKSON_VERSION: Version = VersionUtil.parseVersion("2.13.4", null, null)
+private val BAD_JACKSON_VERSIONS: Map =
+ mapOf("2.18.1" to "due to https://github.com/FasterXML/jackson-databind/issues/4639")
+private val RUNTIME_JACKSON_VERSIONS: List =
+ listOf(
+ com.fasterxml.jackson.core.json.PackageVersion.VERSION,
+ com.fasterxml.jackson.databind.cfg.PackageVersion.VERSION,
+ com.fasterxml.jackson.datatype.jdk8.PackageVersion.VERSION,
+ com.fasterxml.jackson.datatype.jsr310.PackageVersion.VERSION,
+ com.fasterxml.jackson.module.kotlin.PackageVersion.VERSION,
+ )
diff --git a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/ClientOptions.kt b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/ClientOptions.kt
index 370b3f4..07ee264 100644
--- a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/ClientOptions.kt
+++ b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/ClientOptions.kt
@@ -3,71 +3,112 @@
package org.onebusaway.core
import com.fasterxml.jackson.databind.json.JsonMapper
-import com.google.common.collect.ArrayListMultimap
-import com.google.common.collect.ListMultimap
import java.time.Clock
+import org.onebusaway.core.http.Headers
import org.onebusaway.core.http.HttpClient
import org.onebusaway.core.http.PhantomReachableClosingHttpClient
+import org.onebusaway.core.http.QueryParams
import org.onebusaway.core.http.RetryingHttpClient
class ClientOptions
private constructor(
private val originalHttpClient: HttpClient,
@get:JvmName("httpClient") val httpClient: HttpClient,
+ @get:JvmName("checkJacksonVersionCompatibility") val checkJacksonVersionCompatibility: Boolean,
@get:JvmName("jsonMapper") val jsonMapper: JsonMapper,
@get:JvmName("clock") val clock: Clock,
@get:JvmName("baseUrl") val baseUrl: String,
- @get:JvmName("headers") val headers: ListMultimap,
- @get:JvmName("queryParams") val queryParams: ListMultimap,
+ @get:JvmName("headers") val headers: Headers,
+ @get:JvmName("queryParams") val queryParams: QueryParams,
@get:JvmName("responseValidation") val responseValidation: Boolean,
+ @get:JvmName("timeout") val timeout: Timeout,
@get:JvmName("maxRetries") val maxRetries: Int,
@get:JvmName("apiKey") val apiKey: String,
) {
+ init {
+ if (checkJacksonVersionCompatibility) {
+ checkJacksonVersionCompatibility()
+ }
+ }
+
fun toBuilder() = Builder().from(this)
companion object {
const val PRODUCTION_URL = "https://api.pugetsound.onebusaway.org"
+ /**
+ * Returns a mutable builder for constructing an instance of [ClientOptions].
+ *
+ * The following fields are required:
+ * ```java
+ * .httpClient()
+ * .apiKey()
+ * ```
+ */
@JvmStatic fun builder() = Builder()
@JvmStatic fun fromEnv(): ClientOptions = builder().fromEnv().build()
}
- class Builder {
+ /** A builder for [ClientOptions]. */
+ class Builder internal constructor() {
private var httpClient: HttpClient? = null
+ private var checkJacksonVersionCompatibility: Boolean = true
private var jsonMapper: JsonMapper = jsonMapper()
private var clock: Clock = Clock.systemUTC()
private var baseUrl: String = PRODUCTION_URL
- private var headers: ListMultimap = ArrayListMultimap.create()
- private var queryParams: ListMultimap = ArrayListMultimap.create()
+ private var headers: Headers.Builder = Headers.builder()
+ private var queryParams: QueryParams.Builder = QueryParams.builder()
private var responseValidation: Boolean = false
+ private var timeout: Timeout = Timeout.default()
private var maxRetries: Int = 2
private var apiKey: String? = null
@JvmSynthetic
internal fun from(clientOptions: ClientOptions) = apply {
httpClient = clientOptions.originalHttpClient
+ checkJacksonVersionCompatibility = clientOptions.checkJacksonVersionCompatibility
jsonMapper = clientOptions.jsonMapper
clock = clientOptions.clock
baseUrl = clientOptions.baseUrl
- headers = ArrayListMultimap.create(clientOptions.headers)
- queryParams = ArrayListMultimap.create(clientOptions.queryParams)
+ headers = clientOptions.headers.toBuilder()
+ queryParams = clientOptions.queryParams.toBuilder()
responseValidation = clientOptions.responseValidation
+ timeout = clientOptions.timeout
maxRetries = clientOptions.maxRetries
apiKey = clientOptions.apiKey
}
fun httpClient(httpClient: HttpClient) = apply { this.httpClient = httpClient }
+ fun checkJacksonVersionCompatibility(checkJacksonVersionCompatibility: Boolean) = apply {
+ this.checkJacksonVersionCompatibility = checkJacksonVersionCompatibility
+ }
+
fun jsonMapper(jsonMapper: JsonMapper) = apply { this.jsonMapper = jsonMapper }
fun clock(clock: Clock) = apply { this.clock = clock }
fun baseUrl(baseUrl: String) = apply { this.baseUrl = baseUrl }
+ fun responseValidation(responseValidation: Boolean) = apply {
+ this.responseValidation = responseValidation
+ }
+
+ fun timeout(timeout: Timeout) = apply { this.timeout = timeout }
+
+ fun maxRetries(maxRetries: Int) = apply { this.maxRetries = maxRetries }
+
+ fun apiKey(apiKey: String) = apply { this.apiKey = apiKey }
+
+ fun headers(headers: Headers) = apply {
+ this.headers.clear()
+ putAllHeaders(headers)
+ }
+
fun headers(headers: Map>) = apply {
this.headers.clear()
putAllHeaders(headers)
@@ -75,29 +116,34 @@ private constructor(
fun putHeader(name: String, value: String) = apply { headers.put(name, value) }
- fun putHeaders(name: String, values: Iterable) = apply {
- headers.putAll(name, values)
- }
+ fun putHeaders(name: String, values: Iterable) = apply { headers.put(name, values) }
+
+ fun putAllHeaders(headers: Headers) = apply { this.headers.putAll(headers) }
fun putAllHeaders(headers: Map>) = apply {
- headers.forEach(::putHeaders)
+ this.headers.putAll(headers)
}
- fun replaceHeaders(name: String, value: String) = apply {
- headers.replaceValues(name, listOf(value))
- }
+ fun replaceHeaders(name: String, value: String) = apply { headers.replace(name, value) }
fun replaceHeaders(name: String, values: Iterable) = apply {
- headers.replaceValues(name, values)
+ headers.replace(name, values)
}
+ fun replaceAllHeaders(headers: Headers) = apply { this.headers.replaceAll(headers) }
+
fun replaceAllHeaders(headers: Map>) = apply {
- headers.forEach(::replaceHeaders)
+ this.headers.replaceAll(headers)
}
- fun removeHeaders(name: String) = apply { headers.removeAll(name) }
+ fun removeHeaders(name: String) = apply { headers.remove(name) }
- fun removeAllHeaders(names: Set) = apply { names.forEach(::removeHeaders) }
+ fun removeAllHeaders(names: Set) = apply { headers.removeAll(names) }
+
+ fun queryParams(queryParams: QueryParams) = apply {
+ this.queryParams.clear()
+ putAllQueryParams(queryParams)
+ }
fun queryParams(queryParams: Map>) = apply {
this.queryParams.clear()
@@ -107,45 +153,63 @@ private constructor(
fun putQueryParam(key: String, value: String) = apply { queryParams.put(key, value) }
fun putQueryParams(key: String, values: Iterable) = apply {
- queryParams.putAll(key, values)
+ queryParams.put(key, values)
+ }
+
+ fun putAllQueryParams(queryParams: QueryParams) = apply {
+ this.queryParams.putAll(queryParams)
}
fun putAllQueryParams(queryParams: Map>) = apply {
- queryParams.forEach(::putQueryParams)
+ this.queryParams.putAll(queryParams)
}
fun replaceQueryParams(key: String, value: String) = apply {
- queryParams.replaceValues(key, listOf(value))
+ queryParams.replace(key, value)
}
fun replaceQueryParams(key: String, values: Iterable) = apply {
- queryParams.replaceValues(key, values)
+ queryParams.replace(key, values)
}
- fun replaceAllQueryParams(queryParams: Map>) = apply {
- queryParams.forEach(::replaceQueryParams)
+ fun replaceAllQueryParams(queryParams: QueryParams) = apply {
+ this.queryParams.replaceAll(queryParams)
}
- fun removeQueryParams(key: String) = apply { queryParams.removeAll(key) }
-
- fun removeAllQueryParams(keys: Set) = apply { keys.forEach(::removeQueryParams) }
-
- fun responseValidation(responseValidation: Boolean) = apply {
- this.responseValidation = responseValidation
+ fun replaceAllQueryParams(queryParams: Map>) = apply {
+ this.queryParams.replaceAll(queryParams)
}
- fun maxRetries(maxRetries: Int) = apply { this.maxRetries = maxRetries }
+ fun removeQueryParams(key: String) = apply { queryParams.remove(key) }
- fun apiKey(apiKey: String) = apply { this.apiKey = apiKey }
+ fun removeAllQueryParams(keys: Set) = apply { queryParams.removeAll(keys) }
- fun fromEnv() = apply { System.getenv("ONEBUSAWAY_API_KEY")?.let { apiKey(it) } }
+ fun baseUrl(): String = baseUrl
+
+ fun fromEnv() = apply {
+ System.getenv("ONEBUSAWAY_SDK_BASE_URL")?.let { baseUrl(it) }
+ System.getenv("ONEBUSAWAY_API_KEY")?.let { apiKey(it) }
+ }
+ /**
+ * Returns an immutable instance of [ClientOptions].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ *
+ * The following fields are required:
+ * ```java
+ * .httpClient()
+ * .apiKey()
+ * ```
+ *
+ * @throws IllegalStateException if any required field is unset.
+ */
fun build(): ClientOptions {
- checkNotNull(httpClient) { "`httpClient` is required but was not set" }
- checkNotNull(apiKey) { "`apiKey` is required but was not set" }
+ val httpClient = checkRequired("httpClient", httpClient)
+ val apiKey = checkRequired("apiKey", apiKey)
- val headers = ArrayListMultimap.create()
- val queryParams = ArrayListMultimap.create()
+ val headers = Headers.builder()
+ val queryParams = QueryParams.builder()
headers.put("X-Stainless-Lang", "java")
headers.put("X-Stainless-Arch", getOsArch())
headers.put("X-Stainless-OS", getOsName())
@@ -153,29 +217,33 @@ private constructor(
headers.put("X-Stainless-Package-Version", getPackageVersion())
headers.put("X-Stainless-Runtime", "JRE")
headers.put("X-Stainless-Runtime-Version", getJavaVersion())
- if (!apiKey.isNullOrEmpty()) {
- queryParams.put("key", apiKey)
+ apiKey.let {
+ if (!it.isEmpty()) {
+ queryParams.put("key", it)
+ }
}
- this.headers.asMap().forEach(headers::replaceValues)
- this.queryParams.asMap().forEach(queryParams::replaceValues)
+ headers.replaceAll(this.headers.build())
+ queryParams.replaceAll(this.queryParams.build())
return ClientOptions(
- httpClient!!,
+ httpClient,
PhantomReachableClosingHttpClient(
RetryingHttpClient.builder()
- .httpClient(httpClient!!)
+ .httpClient(httpClient)
.clock(clock)
.maxRetries(maxRetries)
.build()
),
+ checkJacksonVersionCompatibility,
jsonMapper,
clock,
baseUrl,
- headers.toImmutable(),
- queryParams.toImmutable(),
+ headers.build(),
+ queryParams.build(),
responseValidation,
+ timeout,
maxRetries,
- apiKey!!,
+ apiKey,
)
}
}
diff --git a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/HttpRequestBodies.kt b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/HttpRequestBodies.kt
deleted file mode 100644
index 6794cf7..0000000
--- a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/HttpRequestBodies.kt
+++ /dev/null
@@ -1,111 +0,0 @@
-@file:JvmName("HttpRequestBodies")
-
-package org.onebusaway.core
-
-import com.fasterxml.jackson.databind.json.JsonMapper
-import java.io.ByteArrayOutputStream
-import java.io.OutputStream
-import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder
-import org.onebusaway.core.http.HttpRequestBody
-import org.onebusaway.errors.OnebusawaySdkException
-
-@JvmSynthetic
-internal inline fun json(
- jsonMapper: JsonMapper,
- value: T,
-): HttpRequestBody {
- return object : HttpRequestBody {
- private var cachedBytes: ByteArray? = null
-
- private fun serialize(): ByteArray {
- if (cachedBytes != null) return cachedBytes!!
-
- val buffer = ByteArrayOutputStream()
- try {
- jsonMapper.writeValue(buffer, value)
- cachedBytes = buffer.toByteArray()
- return cachedBytes!!
- } catch (e: Exception) {
- throw OnebusawaySdkException("Error writing request", e)
- }
- }
-
- override fun writeTo(outputStream: OutputStream) {
- outputStream.write(serialize())
- }
-
- override fun contentType(): String = "application/json"
-
- override fun contentLength(): Long {
- return serialize().size.toLong()
- }
-
- override fun repeatable(): Boolean = true
-
- override fun close() {}
- }
-}
-
-@JvmSynthetic
-internal fun multipartFormData(
- jsonMapper: JsonMapper,
- parts: Array?>
-): HttpRequestBody {
- val builder = MultipartEntityBuilder.create()
- parts.forEach { part ->
- if (part?.value != null) {
- when (part.value) {
- is JsonValue -> {
- val buffer = ByteArrayOutputStream()
- try {
- jsonMapper.writeValue(buffer, part.value)
- } catch (e: Exception) {
- throw OnebusawaySdkException("Error serializing value to json", e)
- }
- builder.addBinaryBody(
- part.name,
- buffer.toByteArray(),
- part.contentType,
- part.filename
- )
- }
- is Boolean ->
- builder.addTextBody(
- part.name,
- if (part.value) "true" else "false",
- part.contentType
- )
- is Int -> builder.addTextBody(part.name, part.value.toString(), part.contentType)
- is Long -> builder.addTextBody(part.name, part.value.toString(), part.contentType)
- is Double -> builder.addTextBody(part.name, part.value.toString(), part.contentType)
- is ByteArray ->
- builder.addBinaryBody(part.name, part.value, part.contentType, part.filename)
- is String -> builder.addTextBody(part.name, part.value, part.contentType)
- is Enum -> builder.addTextBody(part.name, part.value.toString(), part.contentType)
- else ->
- throw IllegalArgumentException(
- "Unsupported content type: ${part.value::class.java.simpleName}"
- )
- }
- }
- }
- val entity = builder.build()
-
- return object : HttpRequestBody {
- override fun writeTo(outputStream: OutputStream) {
- try {
- return entity.writeTo(outputStream)
- } catch (e: Exception) {
- throw OnebusawaySdkException("Error writing request", e)
- }
- }
-
- override fun contentType(): String = entity.contentType
-
- override fun contentLength(): Long = -1
-
- override fun repeatable(): Boolean = entity.isRepeatable
-
- override fun close() = entity.close()
- }
-}
diff --git a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/ObjectMappers.kt b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/ObjectMappers.kt
index 7a4b2f8..d00e718 100644
--- a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/ObjectMappers.kt
+++ b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/ObjectMappers.kt
@@ -3,23 +3,165 @@
package org.onebusaway.core
import com.fasterxml.jackson.annotation.JsonInclude
+import com.fasterxml.jackson.core.JsonGenerator
+import com.fasterxml.jackson.core.JsonParseException
+import com.fasterxml.jackson.core.JsonParser
+import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.DeserializationFeature
+import com.fasterxml.jackson.databind.MapperFeature
import com.fasterxml.jackson.databind.SerializationFeature
-import com.fasterxml.jackson.databind.cfg.CoercionAction.Fail
-import com.fasterxml.jackson.databind.cfg.CoercionInputShape.Integer
+import com.fasterxml.jackson.databind.SerializerProvider
+import com.fasterxml.jackson.databind.cfg.CoercionAction
+import com.fasterxml.jackson.databind.cfg.CoercionInputShape
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.fasterxml.jackson.databind.json.JsonMapper
+import com.fasterxml.jackson.databind.module.SimpleModule
+import com.fasterxml.jackson.databind.type.LogicalType
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
-import com.fasterxml.jackson.module.kotlin.jacksonMapperBuilder
+import com.fasterxml.jackson.module.kotlin.kotlinModule
+import java.io.InputStream
+import java.time.DateTimeException
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.ZonedDateTime
+import java.time.format.DateTimeFormatter
+import java.time.temporal.ChronoField
fun jsonMapper(): JsonMapper =
- jacksonMapperBuilder()
+ JsonMapper.builder()
+ .addModule(kotlinModule())
.addModule(Jdk8Module())
.addModule(JavaTimeModule())
+ .addModule(
+ SimpleModule()
+ .addSerializer(InputStreamSerializer)
+ .addDeserializer(LocalDateTime::class.java, LenientLocalDateTimeDeserializer())
+ )
+ .withCoercionConfig(LogicalType.Boolean) {
+ it.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Integer) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Float) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Textual) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Array) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Collection) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Map) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.POJO) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ }
.serializationInclusion(JsonInclude.Include.NON_ABSENT)
.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
.disable(SerializationFeature.FLUSH_AFTER_WRITE_VALUE)
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.disable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
- .withCoercionConfig(String::class.java) { it.setCoercion(Integer, Fail) }
+ .disable(MapperFeature.ALLOW_COERCION_OF_SCALARS)
+ .disable(MapperFeature.AUTO_DETECT_CREATORS)
+ .disable(MapperFeature.AUTO_DETECT_FIELDS)
+ .disable(MapperFeature.AUTO_DETECT_GETTERS)
+ .disable(MapperFeature.AUTO_DETECT_IS_GETTERS)
+ .disable(MapperFeature.AUTO_DETECT_SETTERS)
.build()
+
+/** A serializer that serializes [InputStream] to bytes. */
+private object InputStreamSerializer : BaseSerializer(InputStream::class) {
+
+ private fun readResolve(): Any = InputStreamSerializer
+
+ override fun serialize(
+ value: InputStream?,
+ gen: JsonGenerator?,
+ serializers: SerializerProvider?,
+ ) {
+ if (value == null) {
+ gen?.writeNull()
+ } else {
+ value.use { gen?.writeBinary(it.readBytes()) }
+ }
+ }
+}
+
+/**
+ * A deserializer that can deserialize [LocalDateTime] from datetimes, dates, and zoned datetimes.
+ */
+private class LenientLocalDateTimeDeserializer :
+ StdDeserializer(LocalDateTime::class.java) {
+
+ companion object {
+
+ private val DATE_TIME_FORMATTERS =
+ listOf(
+ DateTimeFormatter.ISO_LOCAL_DATE_TIME,
+ DateTimeFormatter.ISO_LOCAL_DATE,
+ DateTimeFormatter.ISO_ZONED_DATE_TIME,
+ )
+ }
+
+ override fun logicalType(): LogicalType = LogicalType.DateTime
+
+ override fun deserialize(p: JsonParser, context: DeserializationContext?): LocalDateTime {
+ val exceptions = mutableListOf()
+
+ for (formatter in DATE_TIME_FORMATTERS) {
+ try {
+ val temporal = formatter.parse(p.text)
+
+ return when {
+ !temporal.isSupported(ChronoField.HOUR_OF_DAY) ->
+ LocalDate.from(temporal).atStartOfDay()
+ !temporal.isSupported(ChronoField.OFFSET_SECONDS) ->
+ LocalDateTime.from(temporal)
+ else -> ZonedDateTime.from(temporal).toLocalDateTime()
+ }
+ } catch (e: DateTimeException) {
+ exceptions.add(e)
+ }
+ }
+
+ throw JsonParseException(p, "Cannot parse `LocalDateTime` from value: ${p.text}").apply {
+ exceptions.forEach { addSuppressed(it) }
+ }
+ }
+}
diff --git a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/Params.kt b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/Params.kt
new file mode 100644
index 0000000..48d18a8
--- /dev/null
+++ b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/Params.kt
@@ -0,0 +1,16 @@
+package org.onebusaway.core
+
+import org.onebusaway.core.http.Headers
+import org.onebusaway.core.http.QueryParams
+
+/** An interface representing parameters passed to a service method. */
+interface Params {
+ /** The full set of headers in the parameters, including both fixed and additional headers. */
+ fun _headers(): Headers
+
+ /**
+ * The full set of query params in the parameters, including both fixed and additional query
+ * params.
+ */
+ fun _queryParams(): QueryParams
+}
diff --git a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/PhantomReachable.kt b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/PhantomReachable.kt
index e35ca9a..8391e58 100644
--- a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/PhantomReachable.kt
+++ b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/PhantomReachable.kt
@@ -15,10 +15,20 @@ internal fun closeWhenPhantomReachable(observed: Any, closeable: AutoCloseable)
check(observed !== closeable) {
"`observed` cannot be the same object as `closeable` because it would never become phantom reachable"
}
- closeWhenPhantomReachable?.let { it(observed, closeable::close) }
+ closeWhenPhantomReachable(observed, closeable::close)
}
-private val closeWhenPhantomReachable: ((Any, AutoCloseable) -> Unit)? by lazy {
+/**
+ * Calls [close] when [observed] becomes only phantom reachable.
+ *
+ * This is a wrapper around a Java 9+ [java.lang.ref.Cleaner], or a no-op in older Java versions.
+ */
+@JvmSynthetic
+internal fun closeWhenPhantomReachable(observed: Any, close: () -> Unit) {
+ closeWhenPhantomReachable?.let { it(observed, close) }
+}
+
+private val closeWhenPhantomReachable: ((Any, () -> Unit) -> Unit)? by lazy {
try {
val cleanerClass = Class.forName("java.lang.ref.Cleaner")
val cleanerCreate = cleanerClass.getMethod("create")
@@ -26,9 +36,9 @@ private val closeWhenPhantomReachable: ((Any, AutoCloseable) -> Unit)? by lazy {
cleanerClass.getMethod("register", Any::class.java, Runnable::class.java)
val cleanerObject = cleanerCreate.invoke(null);
- { observed, closeable ->
+ { observed, close ->
try {
- cleanerRegister.invoke(cleanerObject, observed, Runnable { closeable.close() })
+ cleanerRegister.invoke(cleanerObject, observed, Runnable { close() })
} catch (e: ReflectiveOperationException) {
if (e is InvocationTargetException) {
when (val cause = e.cause) {
diff --git a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/PrepareRequest.kt b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/PrepareRequest.kt
new file mode 100644
index 0000000..5367d62
--- /dev/null
+++ b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/PrepareRequest.kt
@@ -0,0 +1,24 @@
+@file:JvmName("PrepareRequest")
+
+package org.onebusaway.core
+
+import java.util.concurrent.CompletableFuture
+import org.onebusaway.core.http.HttpRequest
+
+@JvmSynthetic
+internal fun HttpRequest.prepare(clientOptions: ClientOptions, params: Params): HttpRequest =
+ toBuilder()
+ .putAllQueryParams(clientOptions.queryParams)
+ .replaceAllQueryParams(params._queryParams())
+ .putAllHeaders(clientOptions.headers)
+ .replaceAllHeaders(params._headers())
+ .build()
+
+@JvmSynthetic
+internal fun HttpRequest.prepareAsync(
+ clientOptions: ClientOptions,
+ params: Params,
+): CompletableFuture =
+ // This async version exists to make it easier to add async specific preparation logic in the
+ // future.
+ CompletableFuture.completedFuture(prepare(clientOptions, params))
diff --git a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/RequestOptions.kt b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/RequestOptions.kt
index ec2f645..0967032 100644
--- a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/RequestOptions.kt
+++ b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/RequestOptions.kt
@@ -2,17 +2,7 @@ package org.onebusaway.core
import java.time.Duration
-class RequestOptions
-private constructor(
- val responseValidation: Boolean?,
- val timeout: Duration?,
-) {
- fun applyDefaults(options: RequestOptions): RequestOptions {
- return RequestOptions(
- responseValidation = this.responseValidation ?: options.responseValidation,
- timeout = this.timeout ?: options.timeout,
- )
- }
+class RequestOptions private constructor(val responseValidation: Boolean?, val timeout: Timeout?) {
companion object {
@@ -20,21 +10,37 @@ private constructor(
@JvmStatic fun none() = NONE
+ @JvmSynthetic
+ internal fun from(clientOptions: ClientOptions): RequestOptions =
+ builder()
+ .responseValidation(clientOptions.responseValidation)
+ .timeout(clientOptions.timeout)
+ .build()
+
@JvmStatic fun builder() = Builder()
}
- class Builder {
+ fun applyDefaults(options: RequestOptions): RequestOptions =
+ RequestOptions(
+ responseValidation = responseValidation ?: options.responseValidation,
+ timeout =
+ if (options.timeout != null && timeout != null) timeout.assign(options.timeout)
+ else timeout ?: options.timeout,
+ )
+
+ class Builder internal constructor() {
+
private var responseValidation: Boolean? = null
- private var timeout: Duration? = null
+ private var timeout: Timeout? = null
fun responseValidation(responseValidation: Boolean) = apply {
this.responseValidation = responseValidation
}
- fun timeout(timeout: Duration) = apply { this.timeout = timeout }
+ fun timeout(timeout: Timeout) = apply { this.timeout = timeout }
- fun build(): RequestOptions {
- return RequestOptions(responseValidation, timeout)
- }
+ fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build())
+
+ fun build(): RequestOptions = RequestOptions(responseValidation, timeout)
}
}
diff --git a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/Timeout.kt b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/Timeout.kt
new file mode 100644
index 0000000..db8d478
--- /dev/null
+++ b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/Timeout.kt
@@ -0,0 +1,167 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package org.onebusaway.core
+
+import java.time.Duration
+import java.util.Objects
+import java.util.Optional
+import kotlin.jvm.optionals.getOrNull
+
+/** A class containing timeouts for various processing phases of a request. */
+class Timeout
+private constructor(
+ private val connect: Duration?,
+ private val read: Duration?,
+ private val write: Duration?,
+ private val request: Duration?,
+) {
+
+ /**
+ * The maximum time allowed to establish a connection with a host.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `Duration.ofMinutes(1)`.
+ */
+ fun connect(): Duration = connect ?: Duration.ofMinutes(1)
+
+ /**
+ * The maximum time allowed between two data packets when waiting for the server’s response.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `request()`.
+ */
+ fun read(): Duration = read ?: request()
+
+ /**
+ * The maximum time allowed between two data packets when sending the request to the server.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `request()`.
+ */
+ fun write(): Duration = write ?: request()
+
+ /**
+ * The maximum time allowed for a complete HTTP call, not including retries.
+ *
+ * This includes resolving DNS, connecting, writing the request body, server processing, as well
+ * as reading the response body.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `Duration.ofMinutes(1)`.
+ */
+ fun request(): Duration = request ?: Duration.ofMinutes(1)
+
+ fun toBuilder() = Builder().from(this)
+
+ companion object {
+
+ @JvmStatic fun default() = builder().build()
+
+ /** Returns a mutable builder for constructing an instance of [Timeout]. */
+ @JvmStatic fun builder() = Builder()
+ }
+
+ /** A builder for [Timeout]. */
+ class Builder internal constructor() {
+
+ private var connect: Duration? = null
+ private var read: Duration? = null
+ private var write: Duration? = null
+ private var request: Duration? = null
+
+ @JvmSynthetic
+ internal fun from(timeout: Timeout) = apply {
+ connect = timeout.connect
+ read = timeout.read
+ write = timeout.write
+ request = timeout.request
+ }
+
+ /**
+ * The maximum time allowed to establish a connection with a host.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `Duration.ofMinutes(1)`.
+ */
+ fun connect(connect: Duration?) = apply { this.connect = connect }
+
+ /** Alias for calling [Builder.connect] with `connect.orElse(null)`. */
+ fun connect(connect: Optional) = connect(connect.getOrNull())
+
+ /**
+ * The maximum time allowed between two data packets when waiting for the server’s response.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `request()`.
+ */
+ fun read(read: Duration?) = apply { this.read = read }
+
+ /** Alias for calling [Builder.read] with `read.orElse(null)`. */
+ fun read(read: Optional) = read(read.getOrNull())
+
+ /**
+ * The maximum time allowed between two data packets when sending the request to the server.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `request()`.
+ */
+ fun write(write: Duration?) = apply { this.write = write }
+
+ /** Alias for calling [Builder.write] with `write.orElse(null)`. */
+ fun write(write: Optional) = write(write.getOrNull())
+
+ /**
+ * The maximum time allowed for a complete HTTP call, not including retries.
+ *
+ * This includes resolving DNS, connecting, writing the request body, server processing, as
+ * well as reading the response body.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `Duration.ofMinutes(1)`.
+ */
+ fun request(request: Duration?) = apply { this.request = request }
+
+ /** Alias for calling [Builder.request] with `request.orElse(null)`. */
+ fun request(request: Optional) = request(request.getOrNull())
+
+ /**
+ * Returns an immutable instance of [Timeout].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ */
+ fun build(): Timeout = Timeout(connect, read, write, request)
+ }
+
+ @JvmSynthetic
+ internal fun assign(target: Timeout): Timeout =
+ target
+ .toBuilder()
+ .apply {
+ connect?.let(this::connect)
+ read?.let(this::read)
+ write?.let(this::write)
+ request?.let(this::request)
+ }
+ .build()
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return /* spotless:off */ other is Timeout && connect == other.connect && read == other.read && write == other.write && request == other.request /* spotless:on */
+ }
+
+ override fun hashCode(): Int = /* spotless:off */ Objects.hash(connect, read, write, request) /* spotless:on */
+
+ override fun toString() =
+ "Timeout{connect=$connect, read=$read, write=$write, request=$request}"
+}
diff --git a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/Utils.kt b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/Utils.kt
index 4ccb63c..f887344 100644
--- a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/Utils.kt
+++ b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/Utils.kt
@@ -2,8 +2,6 @@
package org.onebusaway.core
-import com.google.common.collect.ImmutableListMultimap
-import com.google.common.collect.ListMultimap
import java.util.Collections
import java.util.SortedMap
import org.onebusaway.errors.OnebusawaySdkInvalidDataException
@@ -18,15 +16,77 @@ internal fun List.toImmutable(): List =
@JvmSynthetic
internal fun Map.toImmutable(): Map =
- if (isEmpty()) Collections.emptyMap() else Collections.unmodifiableMap(toMap())
+ if (isEmpty()) immutableEmptyMap() else Collections.unmodifiableMap(toMap())
+
+@JvmSynthetic internal fun immutableEmptyMap(): Map = Collections.emptyMap()
@JvmSynthetic
internal fun , V> SortedMap.toImmutable(): SortedMap =
if (isEmpty()) Collections.emptySortedMap()
else Collections.unmodifiableSortedMap(toSortedMap(comparator()))
+/**
+ * Returns all elements that yield the largest value for the given function, or an empty list if
+ * there are zero elements.
+ *
+ * This is similar to [Sequence.maxByOrNull] except it returns _all_ elements that yield the largest
+ * value; not just the first one.
+ */
+@JvmSynthetic
+internal fun > Sequence.allMaxBy(selector: (T) -> R): List {
+ var maxValue: R? = null
+ val maxElements = mutableListOf()
+
+ val iterator = iterator()
+ while (iterator.hasNext()) {
+ val element = iterator.next()
+ val value = selector(element)
+ if (maxValue == null || value > maxValue) {
+ maxValue = value
+ maxElements.clear()
+ maxElements.add(element)
+ } else if (value == maxValue) {
+ maxElements.add(element)
+ }
+ }
+
+ return maxElements
+}
+
+/**
+ * Returns whether [this] is equal to [other].
+ *
+ * This differs from [Object.equals] because it also deeply equates arrays based on their contents,
+ * even when there are arrays directly nested within other arrays.
+ */
+@JvmSynthetic
+internal infix fun Any?.contentEquals(other: Any?): Boolean =
+ arrayOf(this).contentDeepEquals(arrayOf(other))
+
+/**
+ * Returns a hash of the given sequence of [values].
+ *
+ * This differs from [java.util.Objects.hash] because it also deeply hashes arrays based on their
+ * contents, even when there are arrays directly nested within other arrays.
+ */
+@JvmSynthetic internal fun contentHash(vararg values: Any?): Int = values.contentDeepHashCode()
+
+/**
+ * Returns a [String] representation of [this].
+ *
+ * This differs from [Object.toString] because it also deeply stringifies arrays based on their
+ * contents, even when there are arrays directly nested within other arrays.
+ */
@JvmSynthetic
-internal fun ListMultimap.toImmutable(): ListMultimap =
- ImmutableListMultimap.copyOf(this)
+internal fun Any?.contentToString(): String {
+ var string = arrayOf(this).contentDeepToString()
+ if (string.startsWith('[')) {
+ string = string.substring(1)
+ }
+ if (string.endsWith(']')) {
+ string = string.substring(0, string.length - 1)
+ }
+ return string
+}
internal interface Enum
diff --git a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/Values.kt b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/Values.kt
index f5d6cde..a630d24 100644
--- a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/Values.kt
+++ b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/Values.kt
@@ -1,8 +1,6 @@
package org.onebusaway.core
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside
-import com.fasterxml.jackson.annotation.JsonAutoDetect
-import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.core.JsonGenerator
@@ -26,69 +24,146 @@ import com.fasterxml.jackson.databind.node.JsonNodeType.OBJECT
import com.fasterxml.jackson.databind.node.JsonNodeType.POJO
import com.fasterxml.jackson.databind.node.JsonNodeType.STRING
import com.fasterxml.jackson.databind.ser.std.NullSerializer
-import java.nio.charset.Charset
+import java.io.InputStream
import java.util.Objects
import java.util.Optional
-import org.apache.hc.core5.http.ContentType
import org.onebusaway.errors.OnebusawaySdkInvalidDataException
+/**
+ * A class representing a serializable JSON field.
+ *
+ * It can either be a [KnownValue] value of type [T], matching the type the SDK expects, or an
+ * arbitrary JSON value that bypasses the type system (via [JsonValue]).
+ */
@JsonDeserialize(using = JsonField.Deserializer::class)
sealed class JsonField {
+ /**
+ * Returns whether this field is missing, which means it will be omitted from the serialized
+ * JSON entirely.
+ */
fun isMissing(): Boolean = this is JsonMissing
+ /** Whether this field is explicitly set to `null`. */
fun isNull(): Boolean = this is JsonNull
- fun asKnown(): Optional =
- when (this) {
- is KnownValue -> Optional.of(value)
- else -> Optional.empty()
- }
+ /**
+ * Returns an [Optional] containing this field's "known" value, meaning it matches the type the
+ * SDK expects, or an empty [Optional] if this field contains an arbitrary [JsonValue].
+ *
+ * This is the opposite of [asUnknown].
+ */
+ fun asKnown():
+ Optional<
+ // Safe because `Optional` is effectively covariant, but Kotlin doesn't know that.
+ @UnsafeVariance
+ T
+ > = Optional.ofNullable((this as? KnownValue)?.value)
/**
- * If the "known" value (i.e. matching the type that the SDK expects) is returned by the API
- * then this method will return an empty `Optional`, otherwise the returned `Optional` is given
- * a `JsonValue`.
+ * Returns an [Optional] containing this field's arbitrary [JsonValue], meaning it mismatches
+ * the type the SDK expects, or an empty [Optional] if this field contains a "known" value.
+ *
+ * This is the opposite of [asKnown].
*/
- fun asUnknown(): Optional =
- when (this) {
- is JsonValue -> Optional.of(this)
- else -> Optional.empty()
- }
+ fun asUnknown(): Optional = Optional.ofNullable(this as? JsonValue)
+ /**
+ * Returns an [Optional] containing this field's boolean value, or an empty [Optional] if it
+ * doesn't contain a boolean.
+ *
+ * This method checks for both a [KnownValue] containing a boolean and for [JsonBoolean].
+ */
fun asBoolean(): Optional =
when (this) {
is JsonBoolean -> Optional.of(value)
+ is KnownValue -> Optional.ofNullable(value as? Boolean)
else -> Optional.empty()
}
+ /**
+ * Returns an [Optional] containing this field's numerical value, or an empty [Optional] if it
+ * doesn't contain a number.
+ *
+ * This method checks for both a [KnownValue] containing a number and for [JsonNumber].
+ */
fun asNumber(): Optional =
when (this) {
is JsonNumber -> Optional.of(value)
+ is KnownValue -> Optional.ofNullable(value as? Number)
else -> Optional.empty()
}
+ /**
+ * Returns an [Optional] containing this field's string value, or an empty [Optional] if it
+ * doesn't contain a string.
+ *
+ * This method checks for both a [KnownValue] containing a string and for [JsonString].
+ */
fun asString(): Optional =
when (this) {
is JsonString -> Optional.of(value)
+ is KnownValue -> Optional.ofNullable(value as? String)
else -> Optional.empty()
}
fun asStringOrThrow(): String =
- when (this) {
- is JsonString -> value
- else -> throw OnebusawaySdkInvalidDataException("Value is not a string")
- }
+ asString().orElseThrow { OnebusawaySdkInvalidDataException("Value is not a string") }
+ /**
+ * Returns an [Optional] containing this field's list value, or an empty [Optional] if it
+ * doesn't contain a list.
+ *
+ * This method checks for both a [KnownValue] containing a list and for [JsonArray].
+ */
fun asArray(): Optional> =
when (this) {
is JsonArray -> Optional.of(values)
+ is KnownValue ->
+ Optional.ofNullable(
+ (value as? List<*>)?.map {
+ try {
+ JsonValue.from(it)
+ } catch (e: IllegalArgumentException) {
+ // The known value is a list, but not all values are convertible to
+ // `JsonValue`.
+ return Optional.empty()
+ }
+ }
+ )
else -> Optional.empty()
}
+ /**
+ * Returns an [Optional] containing this field's map value, or an empty [Optional] if it doesn't
+ * contain a map.
+ *
+ * This method checks for both a [KnownValue] containing a map and for [JsonObject].
+ */
fun asObject(): Optional> =
when (this) {
is JsonObject -> Optional.of(values)
+ is KnownValue ->
+ Optional.ofNullable(
+ (value as? Map<*, *>)
+ ?.map { (key, value) ->
+ if (key !is String) {
+ return Optional.empty()
+ }
+
+ val jsonValue =
+ try {
+ JsonValue.from(value)
+ } catch (e: IllegalArgumentException) {
+ // The known value is a map, but not all items are convertible
+ // to `JsonValue`.
+ return Optional.empty()
+ }
+
+ key to jsonValue
+ }
+ ?.toMap()
+ )
else -> Optional.empty()
}
@@ -96,20 +171,24 @@ sealed class JsonField {
internal fun getRequired(name: String): T =
when (this) {
is KnownValue -> value
- is JsonMissing -> throw OnebusawaySdkInvalidDataException("'${name}' is not set")
- is JsonNull -> throw OnebusawaySdkInvalidDataException("'${name}' is null")
- else ->
- throw OnebusawaySdkInvalidDataException("'${name}' is invalid, received ${this}")
+ is JsonMissing -> throw OnebusawaySdkInvalidDataException("`$name` is not set")
+ is JsonNull -> throw OnebusawaySdkInvalidDataException("`$name` is null")
+ else -> throw OnebusawaySdkInvalidDataException("`$name` is invalid, received $this")
}
@JvmSynthetic
- internal fun getNullable(name: String): T? =
+ internal fun getOptional(
+ name: String
+ ): Optional<
+ // Safe because `Optional` is effectively covariant, but Kotlin doesn't know that.
+ @UnsafeVariance
+ T
+ > =
when (this) {
- is KnownValue -> value
- is JsonMissing -> null
- is JsonNull -> null
- else ->
- throw OnebusawaySdkInvalidDataException("'${name}' is invalid, received ${this}")
+ is KnownValue -> Optional.of(value)
+ is JsonMissing,
+ is JsonNull -> Optional.empty()
+ else -> throw OnebusawaySdkInvalidDataException("`$name` is invalid, received $this")
}
@JvmSynthetic
@@ -119,19 +198,33 @@ sealed class JsonField {
is JsonValue -> this
}
+ @JvmSynthetic internal fun accept(consume: (T) -> Unit) = asKnown().ifPresent(consume)
+
+ /** Returns the result of calling the [visitor] method corresponding to this field's state. */
fun accept(visitor: Visitor): R =
when (this) {
is KnownValue -> visitor.visitKnown(value)
is JsonValue -> accept(visitor as JsonValue.Visitor)
}
+ /**
+ * An interface that defines how to map each possible state of a `JsonField` to a value of
+ * type [R].
+ */
interface Visitor : JsonValue.Visitor {
+
fun visitKnown(value: T): R = visitDefault()
}
companion object {
+
+ /** Returns a [JsonField] containing the given "known" [value]. */
@JvmStatic fun of(value: T): JsonField = KnownValue.of(value)
+ /**
+ * Returns a [JsonField] containing the given "known" [value], or [JsonNull] if [value] is
+ * null.
+ */
@JvmStatic
fun ofNullable(value: T?): JsonField =
when (value) {
@@ -140,9 +233,13 @@ sealed class JsonField {
}
}
- // This class is a Jackson filter that can be used to exclude missing properties from objects
- // This filter should not be used directly and should instead use the @ExcludeMissing annotation
+ /**
+ * This class is a Jackson filter that can be used to exclude missing properties from objects.
+ * This filter should not be used directly and should instead use the @ExcludeMissing
+ * annotation.
+ */
class IsMissing {
+
override fun equals(other: Any?): Boolean = other is JsonMissing
override fun hashCode(): Int = Objects.hash()
@@ -154,21 +251,22 @@ sealed class JsonField {
override fun createContextual(
context: DeserializationContext,
property: BeanProperty?,
- ): JsonDeserializer> {
- return Deserializer(context.contextualType?.containedType(0))
- }
+ ): JsonDeserializer> = Deserializer(context.contextualType?.containedType(0))
- override fun ObjectCodec.deserialize(node: JsonNode): JsonField<*> {
- return type?.let { tryDeserialize(node, type) }?.let { of(it) }
+ override fun ObjectCodec.deserialize(node: JsonNode): JsonField<*> =
+ type?.let { tryDeserialize(node, type) }?.let { of(it) }
?: JsonValue.fromJsonNode(node)
- }
- override fun getNullValue(context: DeserializationContext): JsonField<*> {
- return JsonNull.of()
- }
+ override fun getNullValue(context: DeserializationContext): JsonField<*> = JsonNull.of()
}
}
+/**
+ * A class representing an arbitrary JSON value.
+ *
+ * It is immutable and assignable to any [JsonField], regardless of its expected type (i.e. its
+ * generic type argument).
+ */
@JsonDeserialize(using = JsonValue.Deserializer::class)
sealed class JsonValue : JsonField() {
@@ -176,6 +274,7 @@ sealed class JsonValue : JsonField() {
fun convert(type: Class): R? = JSON_MAPPER.convertValue(this, type)
+ /** Returns the result of calling the [visitor] method corresponding to this value's variant. */
fun accept(visitor: Visitor): R =
when (this) {
is JsonMissing -> visitor.visitMissing()
@@ -187,7 +286,12 @@ sealed class JsonValue : JsonField() {
is JsonObject -> visitor.visitObject(values)
}
+ /**
+ * An interface that defines how to map each variant state of a [JsonValue] to a value of type
+ * [R].
+ */
interface Visitor {
+
fun visitNull(): R = visitDefault()
fun visitMissing(): R = visitDefault()
@@ -202,15 +306,52 @@ sealed class JsonValue : JsonField() {
fun visitObject(values: Map): R = visitDefault()
- fun visitDefault(): R {
- throw RuntimeException("Unexpected value")
- }
+ /**
+ * The default implementation for unimplemented visitor methods.
+ *
+ * @throws IllegalArgumentException in the default implementation.
+ */
+ fun visitDefault(): R = throw IllegalArgumentException("Unexpected value")
}
companion object {
private val JSON_MAPPER = jsonMapper()
+ /**
+ * Converts the given [value] to a [JsonValue].
+ *
+ * This method works best on primitive types, [List] values, [Map] values, and nested
+ * combinations of these. For example:
+ * ```java
+ * // Create primitive JSON values
+ * JsonValue nullValue = JsonValue.from(null);
+ * JsonValue booleanValue = JsonValue.from(true);
+ * JsonValue numberValue = JsonValue.from(42);
+ * JsonValue stringValue = JsonValue.from("Hello World!");
+ *
+ * // Create a JSON array value equivalent to `["Hello", "World"]`
+ * JsonValue arrayValue = JsonValue.from(List.of("Hello", "World"));
+ *
+ * // Create a JSON object value equivalent to `{ "a": 1, "b": 2 }`
+ * JsonValue objectValue = JsonValue.from(Map.of(
+ * "a", 1,
+ * "b", 2
+ * ));
+ *
+ * // Create an arbitrarily nested JSON equivalent to:
+ * // {
+ * // "a": [1, 2],
+ * // "b": [3, 4]
+ * // }
+ * JsonValue complexValue = JsonValue.from(Map.of(
+ * "a", List.of(1, 2),
+ * "b", List.of(3, 4)
+ * ));
+ * ```
+ *
+ * @throws IllegalArgumentException if [value] is not JSON serializable.
+ */
@JvmStatic
fun from(value: Any?): JsonValue =
when (value) {
@@ -219,6 +360,11 @@ sealed class JsonValue : JsonField() {
else -> JSON_MAPPER.convertValue(value, JsonValue::class.java)
}
+ /**
+ * Returns a [JsonValue] converted from the given Jackson [JsonNode].
+ *
+ * @throws IllegalStateException for unsupported node types.
+ */
@JvmStatic
fun fromJsonNode(node: JsonNode): JsonValue =
when (node.nodeType) {
@@ -240,16 +386,19 @@ sealed class JsonValue : JsonField() {
}
class Deserializer : BaseDeserializer(JsonValue::class) {
- override fun ObjectCodec.deserialize(node: JsonNode): JsonValue {
- return fromJsonNode(node)
- }
- override fun getNullValue(context: DeserializationContext?): JsonValue {
- return JsonNull.of()
- }
+ override fun ObjectCodec.deserialize(node: JsonNode): JsonValue = fromJsonNode(node)
+
+ override fun getNullValue(context: DeserializationContext?): JsonValue = JsonNull.of()
}
}
+/**
+ * A class representing a "known" JSON serializable value of type [T], matching the type the SDK
+ * expects.
+ *
+ * It is assignable to `JsonField`.
+ */
class KnownValue
private constructor(
@com.fasterxml.jackson.annotation.JsonValue @get:JvmName("value") val value: T
@@ -260,52 +409,67 @@ private constructor(
return true
}
- return other is KnownValue<*> && value == other.value
+ return other is KnownValue<*> && value contentEquals other.value
}
- override fun hashCode() = value.hashCode()
+ override fun hashCode() = contentHash(value)
- override fun toString() = value.toString()
+ override fun toString() = value.contentToString()
companion object {
+
+ /** Returns a [KnownValue] containing the given [value]. */
@JsonCreator @JvmStatic fun of(value: T) = KnownValue(value)
}
}
+/**
+ * A [JsonValue] representing an omitted JSON field.
+ *
+ * An instance of this class will cause a JSON field to be omitted from the serialized JSON
+ * entirely.
+ */
@JsonSerialize(using = JsonMissing.Serializer::class)
class JsonMissing : JsonValue() {
override fun toString() = ""
companion object {
+
private val INSTANCE: JsonMissing = JsonMissing()
+ /** Returns the singleton instance of [JsonMissing]. */
@JvmStatic fun of() = INSTANCE
}
class Serializer : BaseSerializer(JsonMissing::class) {
+
override fun serialize(
value: JsonMissing,
generator: JsonGenerator,
- provider: SerializerProvider
+ provider: SerializerProvider,
) {
- throw RuntimeException("JsonMissing cannot be serialized")
+ throw IllegalStateException("JsonMissing cannot be serialized")
}
}
}
+/** A [JsonValue] representing a JSON `null` value. */
@JsonSerialize(using = NullSerializer::class)
class JsonNull : JsonValue() {
override fun toString() = "null"
companion object {
+
private val INSTANCE: JsonNull = JsonNull()
+ /** Returns the singleton instance of [JsonMissing]. */
@JsonCreator @JvmStatic fun of() = INSTANCE
}
}
+/** A [JsonValue] representing a JSON boolean value. */
class JsonBoolean
private constructor(
@get:com.fasterxml.jackson.annotation.JsonValue @get:JvmName("value") val value: Boolean
@@ -324,14 +488,18 @@ private constructor(
override fun toString() = value.toString()
companion object {
+
+ /** Returns a [JsonBoolean] containing the given [value]. */
@JsonCreator @JvmStatic fun of(value: Boolean) = JsonBoolean(value)
}
}
+/** A [JsonValue] representing a JSON number value. */
class JsonNumber
private constructor(
@get:com.fasterxml.jackson.annotation.JsonValue @get:JvmName("value") val value: Number
) : JsonValue() {
+
override fun equals(other: Any?): Boolean {
if (this === other) {
return true
@@ -345,10 +513,13 @@ private constructor(
override fun toString() = value.toString()
companion object {
+
+ /** Returns a [JsonNumber] containing the given [value]. */
@JsonCreator @JvmStatic fun of(value: Number) = JsonNumber(value)
}
}
+/** A [JsonValue] representing a JSON string value. */
class JsonString
private constructor(
@get:com.fasterxml.jackson.annotation.JsonValue @get:JvmName("value") val value: String
@@ -367,10 +538,13 @@ private constructor(
override fun toString() = value
companion object {
+
+ /** Returns a [JsonString] containing the given [value]. */
@JsonCreator @JvmStatic fun of(value: String) = JsonString(value)
}
}
+/** A [JsonValue] representing a JSON array value. */
class JsonArray
private constructor(
@get:com.fasterxml.jackson.annotation.JsonValue
@@ -391,10 +565,13 @@ private constructor(
override fun toString() = values.toString()
companion object {
+
+ /** Returns a [JsonArray] containing the given [values]. */
@JsonCreator @JvmStatic fun of(values: List) = JsonArray(values.toImmutable())
}
}
+/** A [JsonValue] representing a JSON object value. */
class JsonObject
private constructor(
@get:com.fasterxml.jackson.annotation.JsonValue
@@ -415,124 +592,132 @@ private constructor(
override fun toString() = values.toString()
companion object {
+
+ /** Returns a [JsonObject] containing the given [values]. */
@JsonCreator
@JvmStatic
fun of(values: Map) = JsonObject(values.toImmutable())
}
}
+/** A Jackson annotation for excluding fields set to [JsonMissing] from the serialized JSON. */
@JacksonAnnotationsInside
-@JsonInclude(
- JsonInclude.Include.CUSTOM,
- valueFilter = JsonField.IsMissing::class,
-)
+@JsonInclude(JsonInclude.Include.CUSTOM, valueFilter = JsonField.IsMissing::class)
annotation class ExcludeMissing
-@JacksonAnnotationsInside
-@JsonAutoDetect(
- getterVisibility = Visibility.NONE,
- isGetterVisibility = Visibility.NONE,
- setterVisibility = Visibility.NONE,
- creatorVisibility = Visibility.NONE,
- fieldVisibility = Visibility.NONE
-)
-annotation class NoAutoDetect
-
-class MultipartFormValue
-internal constructor(
- val name: String,
- val value: T,
- val contentType: ContentType,
- val filename: String? = null
+/** A class representing a field in a `multipart/form-data` request. */
+class MultipartField
+private constructor(
+ /** A [JsonField] value, which will be serialized to zero or more parts. */
+ @get:com.fasterxml.jackson.annotation.JsonValue @get:JvmName("value") val value: JsonField,
+ /** A content type for the serialized parts. */
+ @get:JvmName("contentType") val contentType: String,
+ private val filename: String?,
) {
- private var hashCode: Int = 0
-
- override fun hashCode(): Int {
- if (hashCode == 0) {
- hashCode =
- Objects.hash(
- name,
- contentType,
- filename,
- when (value) {
- is ByteArray -> value.contentHashCode()
- is String -> value
- is Boolean -> value
- is Long -> value
- is Double -> value
- else -> value?.hashCode()
- }
- )
- }
- return hashCode
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other == null || this.javaClass != other.javaClass) return false
+ companion object {
- other as MultipartFormValue<*>
+ /**
+ * Returns a [MultipartField] containing the given [value] as a [KnownValue].
+ *
+ * [contentType] will be set to `application/octet-stream` if [value] is binary data, or
+ * `text/plain; charset=utf-8` otherwise.
+ */
+ @JvmStatic fun of(value: T?) = builder().value(value).build()
+
+ /**
+ * Returns a [MultipartField] containing the given [value].
+ *
+ * [contentType] will be set to `application/octet-stream` if [value] is binary data, or
+ * `text/plain; charset=utf-8` otherwise.
+ */
+ @JvmStatic fun of(value: JsonField) = builder().value(value).build()
+
+ /**
+ * Returns a mutable builder for constructing an instance of [MultipartField].
+ *
+ * The following fields are required:
+ * ```java
+ * .value()
+ * ```
+ *
+ * If [contentType] is unset, then it will be set to `application/octet-stream` if [value]
+ * is binary data, or `text/plain; charset=utf-8` otherwise.
+ */
+ @JvmStatic fun builder() = Builder()
+ }
- if (name != other.name || contentType != other.contentType || filename != other.filename)
- return false
+ /** Returns the filename directive that will be included in the serialized field. */
+ fun filename(): Optional = Optional.ofNullable(filename)
- return when {
- value is ByteArray && other.value is ByteArray -> value contentEquals other.value
- else -> value?.equals(other.value) ?: (other.value == null)
+ @JvmSynthetic
+ internal fun map(transform: (T) -> R): MultipartField =
+ builder().value(value.map(transform)).contentType(contentType).filename(filename).build()
+
+ /** A builder for [MultipartField]. */
+ class Builder internal constructor() {
+
+ private var value: JsonField? = null
+ private var contentType: String? = null
+ private var filename: String? = null
+
+ fun value(value: JsonField) = apply { this.value = value }
+
+ fun value(value: T?) = value(JsonField.ofNullable(value))
+
+ fun contentType(contentType: String) = apply { this.contentType = contentType }
+
+ fun filename(filename: String?) = apply { this.filename = filename }
+
+ /** Alias for calling [Builder.filename] with `filename.orElse(null)`. */
+ fun filename(filename: Optional) = filename(filename.orElse(null))
+
+ /**
+ * Returns an immutable instance of [MultipartField].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ *
+ * The following fields are required:
+ * ```java
+ * .value()
+ * ```
+ *
+ * If [contentType] is unset, then it will be set to `application/octet-stream` if [value]
+ * is binary data, or `text/plain; charset=utf-8` otherwise.
+ *
+ * @throws IllegalStateException if any required field is unset.
+ */
+ fun build(): MultipartField {
+ val value = checkRequired("value", value)
+ return MultipartField(
+ value,
+ contentType
+ ?: if (
+ value is KnownValue &&
+ (value.value is InputStream || value.value is ByteArray)
+ )
+ "application/octet-stream"
+ else "text/plain; charset=utf-8",
+ filename,
+ )
}
}
- override fun toString(): String =
- "MultipartFormValue{name=$name, contentType=$contentType, filename=$filename, value=${valueToString()}}"
+ private val hashCode: Int by lazy { contentHash(value, contentType, filename) }
+
+ override fun hashCode(): Int = hashCode
- private fun valueToString(): String =
- when (value) {
- is ByteArray -> "ByteArray of size ${value.size}"
- else -> value.toString()
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
}
- companion object {
- internal fun fromString(
- name: String,
- value: String,
- contentType: ContentType
- ): MultipartFormValue = MultipartFormValue(name, value, contentType)
-
- internal fun fromBoolean(
- name: String,
- value: Boolean,
- contentType: ContentType,
- ): MultipartFormValue = MultipartFormValue(name, value, contentType)
-
- internal fun fromLong(
- name: String,
- value: Long,
- contentType: ContentType,
- ): MultipartFormValue = MultipartFormValue(name, value, contentType)
-
- internal fun fromDouble(
- name: String,
- value: Double,
- contentType: ContentType,
- ): MultipartFormValue = MultipartFormValue(name, value, contentType)
-
- internal fun fromEnum(
- name: String,
- value: T,
- contentType: ContentType
- ): MultipartFormValue = MultipartFormValue(name, value, contentType)
-
- internal fun fromByteArray(
- name: String,
- value: ByteArray,
- contentType: ContentType,
- filename: String? = null
- ): MultipartFormValue = MultipartFormValue(name, value, contentType, filename)
+ return other is MultipartField<*> &&
+ value == other.value &&
+ contentType == other.contentType &&
+ filename == other.filename
}
-}
-internal object ContentTypes {
- val DefaultText = ContentType.create(ContentType.TEXT_PLAIN.mimeType, Charset.forName("UTF-8"))
- val DefaultBinary = ContentType.DEFAULT_BINARY
+ override fun toString(): String =
+ "MultipartField{value=$value, contentType=$contentType, filename=$filename}"
}
diff --git a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/handlers/ErrorHandler.kt b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/handlers/ErrorHandler.kt
index f7938ba..2d6cd63 100644
--- a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/handlers/ErrorHandler.kt
+++ b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/handlers/ErrorHandler.kt
@@ -1,17 +1,17 @@
+// File generated from our OpenAPI spec by Stainless.
+
@file:JvmName("ErrorHandler")
package org.onebusaway.core.handlers
import com.fasterxml.jackson.databind.json.JsonMapper
-import com.google.common.collect.ListMultimap
-import java.io.ByteArrayInputStream
-import java.io.InputStream
+import org.onebusaway.core.JsonMissing
+import org.onebusaway.core.JsonValue
import org.onebusaway.core.http.HttpResponse
import org.onebusaway.core.http.HttpResponse.Handler
import org.onebusaway.errors.BadRequestException
import org.onebusaway.errors.InternalServerException
import org.onebusaway.errors.NotFoundException
-import org.onebusaway.errors.OnebusawaySdkError
import org.onebusaway.errors.PermissionDeniedException
import org.onebusaway.errors.RateLimitException
import org.onebusaway.errors.UnauthorizedException
@@ -19,109 +19,66 @@ import org.onebusaway.errors.UnexpectedStatusCodeException
import org.onebusaway.errors.UnprocessableEntityException
@JvmSynthetic
-internal fun errorHandler(jsonMapper: JsonMapper): Handler {
- val handler = jsonHandler(jsonMapper)
+internal fun errorHandler(jsonMapper: JsonMapper): Handler {
+ val handler = jsonHandler(jsonMapper)
- return object : Handler {
- override fun handle(response: HttpResponse): OnebusawaySdkError =
+ return object : Handler {
+ override fun handle(response: HttpResponse): JsonValue =
try {
handler.handle(response)
} catch (e: Exception) {
- OnebusawaySdkError.builder().build()
+ JsonMissing.of()
}
}
}
@JvmSynthetic
-internal fun Handler.withErrorHandler(
- errorHandler: Handler
-): Handler =
+internal fun Handler.withErrorHandler(errorHandler: Handler): Handler =
object : Handler {
- override fun handle(response: HttpResponse): T {
+ override fun handle(response: HttpResponse): T =
when (val statusCode = response.statusCode()) {
- in 200..299 -> {
- return this@withErrorHandler.handle(response)
- }
- 400 -> {
- val buffered = response.buffered()
- throw BadRequestException(
- buffered.headers(),
- stringHandler().handle(buffered),
- errorHandler.handle(buffered),
- )
- }
- 401 -> {
- val buffered = response.buffered()
- throw UnauthorizedException(
- buffered.headers(),
- stringHandler().handle(buffered),
- errorHandler.handle(buffered),
- )
- }
- 403 -> {
- val buffered = response.buffered()
- throw PermissionDeniedException(
- buffered.headers(),
- stringHandler().handle(buffered),
- errorHandler.handle(buffered),
- )
- }
- 404 -> {
- val buffered = response.buffered()
- throw NotFoundException(
- buffered.headers(),
- stringHandler().handle(buffered),
- errorHandler.handle(buffered),
- )
- }
- 422 -> {
- val buffered = response.buffered()
- throw UnprocessableEntityException(
- buffered.headers(),
- stringHandler().handle(buffered),
- errorHandler.handle(buffered),
- )
- }
- 429 -> {
- val buffered = response.buffered()
- throw RateLimitException(
- buffered.headers(),
- stringHandler().handle(buffered),
- errorHandler.handle(buffered),
- )
- }
- in 500..599 -> {
- val buffered = response.buffered()
- throw InternalServerException(
- statusCode,
- buffered.headers(),
- stringHandler().handle(buffered),
- errorHandler.handle(buffered),
- )
- }
- else -> {
- val buffered = response.buffered()
- throw UnexpectedStatusCodeException(
- statusCode,
- buffered.headers(),
- stringHandler().handle(buffered),
- errorHandler.handle(buffered),
- )
- }
+ in 200..299 -> this@withErrorHandler.handle(response)
+ 400 ->
+ throw BadRequestException.builder()
+ .headers(response.headers())
+ .body(errorHandler.handle(response))
+ .build()
+ 401 ->
+ throw UnauthorizedException.builder()
+ .headers(response.headers())
+ .body(errorHandler.handle(response))
+ .build()
+ 403 ->
+ throw PermissionDeniedException.builder()
+ .headers(response.headers())
+ .body(errorHandler.handle(response))
+ .build()
+ 404 ->
+ throw NotFoundException.builder()
+ .headers(response.headers())
+ .body(errorHandler.handle(response))
+ .build()
+ 422 ->
+ throw UnprocessableEntityException.builder()
+ .headers(response.headers())
+ .body(errorHandler.handle(response))
+ .build()
+ 429 ->
+ throw RateLimitException.builder()
+ .headers(response.headers())
+ .body(errorHandler.handle(response))
+ .build()
+ in 500..599 ->
+ throw InternalServerException.builder()
+ .statusCode(statusCode)
+ .headers(response.headers())
+ .body(errorHandler.handle(response))
+ .build()
+ else ->
+ throw UnexpectedStatusCodeException.builder()
+ .statusCode(statusCode)
+ .headers(response.headers())
+ .body(errorHandler.handle(response))
+ .build()
}
- }
- }
-
-private fun HttpResponse.buffered(): HttpResponse {
- val body = body().readBytes()
-
- return object : HttpResponse {
- override fun statusCode(): Int = this@buffered.statusCode()
-
- override fun headers(): ListMultimap = this@buffered.headers()
-
- override fun body(): InputStream = ByteArrayInputStream(body)
-
- override fun close() = this@buffered.close()
}
-}
diff --git a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/handlers/JsonHandler.kt b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/handlers/JsonHandler.kt
index 6ae84af..18b8ac9 100644
--- a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/handlers/JsonHandler.kt
+++ b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/handlers/JsonHandler.kt
@@ -6,16 +6,15 @@ import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.kotlin.jacksonTypeRef
import org.onebusaway.core.http.HttpResponse
import org.onebusaway.core.http.HttpResponse.Handler
-import org.onebusaway.errors.OnebusawaySdkException
+import org.onebusaway.errors.OnebusawaySdkInvalidDataException
@JvmSynthetic
internal inline fun jsonHandler(jsonMapper: JsonMapper): Handler =
object : Handler {
- override fun handle(response: HttpResponse): T {
+ override fun handle(response: HttpResponse): T =
try {
- return jsonMapper.readValue(response.body(), jacksonTypeRef())
+ jsonMapper.readValue(response.body(), jacksonTypeRef())
} catch (e: Exception) {
- throw OnebusawaySdkException("Error reading response", e)
+ throw OnebusawaySdkInvalidDataException("Error reading response", e)
}
- }
}
diff --git a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/AsyncStreamResponse.kt b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/AsyncStreamResponse.kt
new file mode 100644
index 0000000..2f97f46
--- /dev/null
+++ b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/AsyncStreamResponse.kt
@@ -0,0 +1,157 @@
+package org.onebusaway.core.http
+
+import java.util.Optional
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.Executor
+import java.util.concurrent.atomic.AtomicReference
+import org.onebusaway.core.http.AsyncStreamResponse.Handler
+
+/**
+ * A class providing access to an API response as an asynchronous stream of chunks of type [T],
+ * where each chunk can be individually processed as soon as it arrives instead of waiting on the
+ * full response.
+ */
+interface AsyncStreamResponse {
+
+ /**
+ * Registers [handler] to be called for events of this stream.
+ *
+ * [handler]'s methods will be called in the client's configured or default thread pool.
+ *
+ * @throws IllegalStateException if [subscribe] has already been called.
+ */
+ fun subscribe(handler: Handler): AsyncStreamResponse
+
+ /**
+ * Registers [handler] to be called for events of this stream.
+ *
+ * [handler]'s methods will be called in the given [executor].
+ *
+ * @throws IllegalStateException if [subscribe] has already been called.
+ */
+ fun subscribe(handler: Handler, executor: Executor): AsyncStreamResponse
+
+ /**
+ * Returns a future that completes when a stream is fully consumed, errors, or gets closed
+ * early.
+ */
+ fun onCompleteFuture(): CompletableFuture
+
+ /**
+ * Closes this resource, relinquishing any underlying resources.
+ *
+ * This is purposefully not inherited from [AutoCloseable] because this response should not be
+ * synchronously closed via try-with-resources.
+ */
+ fun close()
+
+ /** A class for handling streaming events. */
+ fun interface Handler {
+
+ /** Called whenever a chunk is received. */
+ fun onNext(value: T)
+
+ /**
+ * Called when a stream is fully consumed, errors, or gets closed early.
+ *
+ * [onNext] will not be called once this method is called.
+ *
+ * @param error Non-empty if the stream completed due to an error.
+ */
+ fun onComplete(error: Optional) {}
+ }
+}
+
+@JvmSynthetic
+internal fun CompletableFuture>.toAsync(streamHandlerExecutor: Executor) =
+ PhantomReachableClosingAsyncStreamResponse(
+ object : AsyncStreamResponse {
+
+ private val onCompleteFuture = CompletableFuture()
+ private val state = AtomicReference(State.NEW)
+
+ init {
+ this@toAsync.whenComplete { _, error ->
+ // If an error occurs from the original future, then we should resolve the
+ // `onCompleteFuture` even if `subscribe` has not been called.
+ error?.let(onCompleteFuture::completeExceptionally)
+ }
+ }
+
+ override fun subscribe(handler: Handler): AsyncStreamResponse =
+ subscribe(handler, streamHandlerExecutor)
+
+ override fun subscribe(
+ handler: Handler,
+ executor: Executor,
+ ): AsyncStreamResponse = apply {
+ // TODO(JDK): Use `compareAndExchange` once targeting JDK 9.
+ check(state.compareAndSet(State.NEW, State.SUBSCRIBED)) {
+ if (state.get() == State.SUBSCRIBED) "Cannot subscribe more than once"
+ else "Cannot subscribe after the response is closed"
+ }
+
+ this@toAsync.whenCompleteAsync(
+ { streamResponse, futureError ->
+ if (state.get() == State.CLOSED) {
+ // Avoid doing any work if `close` was called before the future
+ // completed.
+ return@whenCompleteAsync
+ }
+
+ if (futureError != null) {
+ // An error occurred before we started passing chunks to the handler.
+ handler.onComplete(Optional.of(futureError))
+ return@whenCompleteAsync
+ }
+
+ var streamError: Throwable? = null
+ try {
+ streamResponse.stream().forEach(handler::onNext)
+ } catch (e: Throwable) {
+ streamError = e
+ }
+
+ try {
+ handler.onComplete(Optional.ofNullable(streamError))
+ } finally {
+ try {
+ // Notify completion via the `onCompleteFuture` as well. This is in
+ // a separate `try-finally` block so that we still complete the
+ // future if `handler.onComplete` throws.
+ if (streamError == null) {
+ onCompleteFuture.complete(null)
+ } else {
+ onCompleteFuture.completeExceptionally(streamError)
+ }
+ } finally {
+ close()
+ }
+ }
+ },
+ executor,
+ )
+ }
+
+ override fun onCompleteFuture(): CompletableFuture = onCompleteFuture
+
+ override fun close() {
+ val previousState = state.getAndSet(State.CLOSED)
+ if (previousState == State.CLOSED) {
+ return
+ }
+
+ this@toAsync.whenComplete { streamResponse, error -> streamResponse?.close() }
+ // When the stream is closed, we should always consider it closed. If it closed due
+ // to an error, then we will have already completed the future earlier, and this
+ // will be a no-op.
+ onCompleteFuture.complete(null)
+ }
+ }
+ )
+
+private enum class State {
+ NEW,
+ SUBSCRIBED,
+ CLOSED,
+}
diff --git a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/Headers.kt b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/Headers.kt
index 1e17303..c3820c3 100644
--- a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/Headers.kt
+++ b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/Headers.kt
@@ -6,7 +6,7 @@ import org.onebusaway.core.toImmutable
class Headers
private constructor(
private val map: Map>,
- @get:JvmName("size") val size: Int
+ @get:JvmName("size") val size: Int,
) {
fun isEmpty(): Boolean = map.isEmpty()
@@ -22,7 +22,7 @@ private constructor(
@JvmStatic fun builder() = Builder()
}
- class Builder {
+ class Builder internal constructor() {
private val map: MutableMap> =
TreeMap(String.CASE_INSENSITIVE_ORDER)
@@ -74,7 +74,7 @@ private constructor(
values.toImmutable()
}
.toImmutable(),
- size
+ size,
)
}
diff --git a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/HttpClient.kt b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/HttpClient.kt
index bb0f294..aa57cf6 100644
--- a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/HttpClient.kt
+++ b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/HttpClient.kt
@@ -1,5 +1,3 @@
-@file:Suppress("OVERLOADS_INTERFACE") // See https://youtrack.jetbrains.com/issue/KT-36102
-
package org.onebusaway.core.http
import java.lang.AutoCloseable
@@ -8,15 +6,21 @@ import org.onebusaway.core.RequestOptions
interface HttpClient : AutoCloseable {
- @JvmOverloads
fun execute(
request: HttpRequest,
requestOptions: RequestOptions = RequestOptions.none(),
): HttpResponse
- @JvmOverloads
+ fun execute(request: HttpRequest): HttpResponse = execute(request, RequestOptions.none())
+
fun executeAsync(
request: HttpRequest,
requestOptions: RequestOptions = RequestOptions.none(),
): CompletableFuture
+
+ fun executeAsync(request: HttpRequest): CompletableFuture =
+ executeAsync(request, RequestOptions.none())
+
+ /** Overridden from [AutoCloseable] to not have a checked exception in its signature. */
+ override fun close()
}
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 9139930..1b4396b 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,8 +1,6 @@
package org.onebusaway.core.http
-import com.google.common.collect.ArrayListMultimap
-import com.google.common.collect.ListMultimap
-import com.google.common.collect.MultimapBuilder
+import org.onebusaway.core.checkRequired
import org.onebusaway.core.toImmutable
class HttpRequest
@@ -10,11 +8,13 @@ private constructor(
@get:JvmName("method") val method: HttpMethod,
@get:JvmName("url") val url: String?,
@get:JvmName("pathSegments") val pathSegments: List,
- @get:JvmName("headers") val headers: ListMultimap,
- @get:JvmName("queryParams") val queryParams: ListMultimap,
+ @get:JvmName("headers") val headers: Headers,
+ @get:JvmName("queryParams") val queryParams: QueryParams,
@get:JvmName("body") val body: HttpRequestBody?,
) {
+ fun toBuilder(): Builder = Builder().from(this)
+
override fun toString(): String =
"HttpRequest{method=$method, url=$url, pathSegments=$pathSegments, headers=$headers, queryParams=$queryParams, body=$body}"
@@ -22,16 +22,25 @@ private constructor(
@JvmStatic fun builder() = Builder()
}
- class Builder {
+ class Builder internal constructor() {
private var method: HttpMethod? = null
private var url: String? = null
private var pathSegments: MutableList = mutableListOf()
- private var headers: ListMultimap =
- MultimapBuilder.treeKeys(String.CASE_INSENSITIVE_ORDER).arrayListValues().build()
- private var queryParams: ListMultimap = ArrayListMultimap.create()
+ private var headers: Headers.Builder = Headers.builder()
+ private var queryParams: QueryParams.Builder = QueryParams.builder()
private var body: HttpRequestBody? = null
+ @JvmSynthetic
+ internal fun from(request: HttpRequest) = apply {
+ method = request.method
+ url = request.url
+ pathSegments = request.pathSegments.toMutableList()
+ headers = request.headers.toBuilder()
+ queryParams = request.queryParams.toBuilder()
+ body = request.body
+ }
+
fun method(method: HttpMethod) = apply { this.method = method }
fun url(url: String) = apply { this.url = url }
@@ -42,6 +51,11 @@ private constructor(
this.pathSegments.addAll(pathSegments)
}
+ fun headers(headers: Headers) = apply {
+ this.headers.clear()
+ putAllHeaders(headers)
+ }
+
fun headers(headers: Map>) = apply {
this.headers.clear()
putAllHeaders(headers)
@@ -49,29 +63,34 @@ private constructor(
fun putHeader(name: String, value: String) = apply { headers.put(name, value) }
- fun putHeaders(name: String, values: Iterable) = apply {
- headers.putAll(name, values)
- }
+ fun putHeaders(name: String, values: Iterable) = apply { headers.put(name, values) }
+
+ fun putAllHeaders(headers: Headers) = apply { this.headers.putAll(headers) }
fun putAllHeaders(headers: Map>) = apply {
- headers.forEach(::putHeaders)
+ this.headers.putAll(headers)
}
- fun replaceHeaders(name: String, value: String) = apply {
- headers.replaceValues(name, listOf(value))
- }
+ fun replaceHeaders(name: String, value: String) = apply { headers.replace(name, value) }
fun replaceHeaders(name: String, values: Iterable) = apply {
- headers.replaceValues(name, values)
+ headers.replace(name, values)
}
+ fun replaceAllHeaders(headers: Headers) = apply { this.headers.replaceAll(headers) }
+
fun replaceAllHeaders(headers: Map>) = apply {
- headers.forEach(::replaceHeaders)
+ this.headers.replaceAll(headers)
}
- fun removeHeaders(name: String) = apply { headers.removeAll(name) }
+ fun removeHeaders(name: String) = apply { headers.remove(name) }
+
+ fun removeAllHeaders(names: Set) = apply { headers.removeAll(names) }
- fun removeAllHeaders(names: Set) = apply { names.forEach(::removeHeaders) }
+ fun queryParams(queryParams: QueryParams) = apply {
+ this.queryParams.clear()
+ putAllQueryParams(queryParams)
+ }
fun queryParams(queryParams: Map>) = apply {
this.queryParams.clear()
@@ -81,38 +100,46 @@ private constructor(
fun putQueryParam(key: String, value: String) = apply { queryParams.put(key, value) }
fun putQueryParams(key: String, values: Iterable) = apply {
- queryParams.putAll(key, values)
+ queryParams.put(key, values)
+ }
+
+ fun putAllQueryParams(queryParams: QueryParams) = apply {
+ this.queryParams.putAll(queryParams)
}
fun putAllQueryParams(queryParams: Map>) = apply {
- queryParams.forEach(::putQueryParams)
+ this.queryParams.putAll(queryParams)
}
fun replaceQueryParams(key: String, value: String) = apply {
- queryParams.replaceValues(key, listOf(value))
+ queryParams.replace(key, value)
}
fun replaceQueryParams(key: String, values: Iterable) = apply {
- queryParams.replaceValues(key, values)
+ queryParams.replace(key, values)
+ }
+
+ fun replaceAllQueryParams(queryParams: QueryParams) = apply {
+ this.queryParams.replaceAll(queryParams)
}
fun replaceAllQueryParams(queryParams: Map>) = apply {
- queryParams.forEach(::replaceQueryParams)
+ this.queryParams.replaceAll(queryParams)
}
- fun removeQueryParams(key: String) = apply { queryParams.removeAll(key) }
+ fun removeQueryParams(key: String) = apply { queryParams.remove(key) }
- fun removeAllQueryParams(keys: Set) = apply { keys.forEach(::removeQueryParams) }
+ fun removeAllQueryParams(keys: Set) = apply { queryParams.removeAll(keys) }
fun body(body: HttpRequestBody) = apply { this.body = body }
fun build(): HttpRequest =
HttpRequest(
- checkNotNull(method) { "`method` is required but was not set" },
+ checkRequired("method", method),
url,
pathSegments.toImmutable(),
- headers,
- queryParams.toImmutable(),
+ headers.build(),
+ queryParams.build(),
body,
)
}
diff --git a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/HttpRequestBodies.kt b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/HttpRequestBodies.kt
new file mode 100644
index 0000000..0822ec1
--- /dev/null
+++ b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/HttpRequestBodies.kt
@@ -0,0 +1,106 @@
+// File generated from our OpenAPI spec by Stainless.
+
+@file:JvmName("HttpRequestBodies")
+
+package org.onebusaway.core.http
+
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.json.JsonMapper
+import com.fasterxml.jackson.databind.node.JsonNodeType
+import java.io.InputStream
+import java.io.OutputStream
+import kotlin.jvm.optionals.getOrNull
+import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder
+import org.apache.hc.core5.http.ContentType
+import org.apache.hc.core5.http.HttpEntity
+import org.onebusaway.core.MultipartField
+import org.onebusaway.errors.OnebusawaySdkInvalidDataException
+
+@JvmSynthetic
+internal inline fun json(jsonMapper: JsonMapper, value: T): HttpRequestBody =
+ object : HttpRequestBody {
+ private val bytes: ByteArray by lazy { jsonMapper.writeValueAsBytes(value) }
+
+ override fun writeTo(outputStream: OutputStream) = outputStream.write(bytes)
+
+ override fun contentType(): String = "application/json"
+
+ override fun contentLength(): Long = bytes.size.toLong()
+
+ override fun repeatable(): Boolean = true
+
+ override fun close() {}
+ }
+
+@JvmSynthetic
+internal fun multipartFormData(
+ jsonMapper: JsonMapper,
+ fields: Map>,
+): HttpRequestBody =
+ object : HttpRequestBody {
+ private val entity: HttpEntity by lazy {
+ MultipartEntityBuilder.create()
+ .apply {
+ fields.forEach { (name, field) ->
+ val knownValue = field.value.asKnown().getOrNull()
+ val parts =
+ if (knownValue is InputStream) {
+ // Read directly from the `InputStream` instead of reading it all
+ // into memory due to the `jsonMapper` serialization below.
+ sequenceOf(name to knownValue)
+ } else {
+ val node = jsonMapper.valueToTree(field.value)
+ serializePart(name, node)
+ }
+
+ parts.forEach { (name, bytes) ->
+ addBinaryBody(
+ name,
+ bytes,
+ ContentType.parseLenient(field.contentType),
+ field.filename().getOrNull(),
+ )
+ }
+ }
+ }
+ .build()
+ }
+
+ private fun serializePart(
+ name: String,
+ node: JsonNode,
+ ): Sequence> =
+ when (node.nodeType) {
+ JsonNodeType.MISSING,
+ JsonNodeType.NULL -> emptySequence()
+ JsonNodeType.BINARY -> sequenceOf(name to node.binaryValue().inputStream())
+ JsonNodeType.STRING -> sequenceOf(name to node.textValue().inputStream())
+ JsonNodeType.BOOLEAN ->
+ sequenceOf(name to node.booleanValue().toString().inputStream())
+ JsonNodeType.NUMBER ->
+ sequenceOf(name to node.numberValue().toString().inputStream())
+ JsonNodeType.ARRAY ->
+ node.elements().asSequence().flatMap { element -> serializePart(name, element) }
+ JsonNodeType.OBJECT ->
+ node.fields().asSequence().flatMap { (key, value) ->
+ serializePart("$name[$key]", value)
+ }
+ JsonNodeType.POJO,
+ null ->
+ throw OnebusawaySdkInvalidDataException(
+ "Unexpected JsonNode type: ${node.nodeType}"
+ )
+ }
+
+ private fun String.inputStream(): InputStream = toByteArray().inputStream()
+
+ override fun writeTo(outputStream: OutputStream) = entity.writeTo(outputStream)
+
+ override fun contentType(): String = entity.contentType
+
+ override fun contentLength(): Long = entity.contentLength
+
+ override fun repeatable(): Boolean = entity.isRepeatable
+
+ override fun close() = entity.close()
+ }
diff --git a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/HttpRequestBody.kt b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/HttpRequestBody.kt
index b9617f4..4086d94 100644
--- a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/HttpRequestBody.kt
+++ b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/HttpRequestBody.kt
@@ -1,12 +1,11 @@
package org.onebusaway.core.http
-import java.io.IOException
import java.io.OutputStream
import java.lang.AutoCloseable
interface HttpRequestBody : AutoCloseable {
- @Throws(IOException::class) fun writeTo(outputStream: OutputStream)
+ fun writeTo(outputStream: OutputStream)
fun contentType(): String?
@@ -21,5 +20,6 @@ interface HttpRequestBody : AutoCloseable {
*/
fun repeatable(): Boolean
+ /** Overridden from [AutoCloseable] to not have a checked exception in its signature. */
override fun close()
}
diff --git a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/HttpResponse.kt b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/HttpResponse.kt
index b2870ca..275cbd4 100644
--- a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/HttpResponse.kt
+++ b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/HttpResponse.kt
@@ -1,17 +1,20 @@
+// File generated from our OpenAPI spec by Stainless.
+
package org.onebusaway.core.http
-import com.google.common.collect.ListMultimap
import java.io.InputStream
-import java.lang.AutoCloseable
interface HttpResponse : AutoCloseable {
fun statusCode(): Int
- fun headers(): ListMultimap
+ fun headers(): Headers
fun body(): InputStream
+ /** Overridden from [AutoCloseable] to not have a checked exception in its signature. */
+ override fun close()
+
interface Handler {
fun handle(response: HttpResponse): T
diff --git a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/HttpResponseFor.kt b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/HttpResponseFor.kt
new file mode 100644
index 0000000..e63df9d
--- /dev/null
+++ b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/HttpResponseFor.kt
@@ -0,0 +1,25 @@
+package org.onebusaway.core.http
+
+import java.io.InputStream
+
+interface HttpResponseFor : HttpResponse {
+
+ fun parse(): T
+}
+
+@JvmSynthetic
+internal fun HttpResponse.parseable(parse: () -> T): HttpResponseFor =
+ object : HttpResponseFor {
+
+ private val parsed: T by lazy { parse() }
+
+ override fun parse(): T = parsed
+
+ override fun statusCode(): Int = this@parseable.statusCode()
+
+ override fun headers(): Headers = this@parseable.headers()
+
+ override fun body(): InputStream = this@parseable.body()
+
+ override fun close() = this@parseable.close()
+ }
diff --git a/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/PhantomReachableClosingAsyncStreamResponse.kt b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/PhantomReachableClosingAsyncStreamResponse.kt
new file mode 100644
index 0000000..ae64979
--- /dev/null
+++ b/onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/PhantomReachableClosingAsyncStreamResponse.kt
@@ -0,0 +1,56 @@
+package org.onebusaway.core.http
+
+import java.util.Optional
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.Executor
+import org.onebusaway.core.closeWhenPhantomReachable
+import org.onebusaway.core.http.AsyncStreamResponse.Handler
+
+/**
+ * A delegating wrapper around an `AsyncStreamResponse` that closes it once it's only phantom
+ * reachable.
+ *
+ * This class ensures the `AsyncStreamResponse` is closed even if the user forgets to close it.
+ */
+internal class PhantomReachableClosingAsyncStreamResponse(
+ private val asyncStreamResponse: AsyncStreamResponse
+) : AsyncStreamResponse {
+
+ /**
+ * An object used for keeping `asyncStreamResponse` open while the object is still reachable.
+ */
+ private val reachabilityTracker = Object()
+
+ init {
+ closeWhenPhantomReachable(reachabilityTracker, asyncStreamResponse::close)
+ }
+
+ override fun subscribe(handler: Handler): AsyncStreamResponse = apply {
+ asyncStreamResponse.subscribe(TrackedHandler(handler, reachabilityTracker))
+ }
+
+ override fun subscribe(handler: Handler, executor: Executor): AsyncStreamResponse