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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.1.0-alpha.53"
".": "0.1.0-alpha.54"
}
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## 0.1.0-alpha.54 (2026-02-25)

Full Changelog: [v0.1.0-alpha.53...v0.1.0-alpha.54](https://github.com/OneBusAway/java-sdk/compare/v0.1.0-alpha.53...v0.1.0-alpha.54)

### Chores

* drop apache dependency ([7c4dc22](https://github.com/OneBusAway/java-sdk/commit/7c4dc22dafc8ce6892a94da4d227d5e0a810c4ce))
* **internal:** expand imports ([6b02ef8](https://github.com/OneBusAway/java-sdk/commit/6b02ef86864a0f6a2a1456f59c017364fa271a40))
* make `Properties` more resilient to `null` ([a743178](https://github.com/OneBusAway/java-sdk/commit/a7431780bc7a3b13b284ab66bfafc7aee55dd802))

## 0.1.0-alpha.53 (2026-02-19)

Full Changelog: [v0.1.0-alpha.52...v0.1.0-alpha.53](https://github.com/OneBusAway/java-sdk/compare/v0.1.0-alpha.52...v0.1.0-alpha.53)
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

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

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

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

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

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

The REST API documentation can be found on [developer.onebusaway.org](https://developer.onebusaway.org). Javadocs are available on [javadoc.io](https://javadoc.io/doc/org.onebusaway/onebusaway-sdk-java/0.1.0-alpha.53).
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.54).

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

Expand All @@ -26,7 +26,7 @@ The REST API documentation can be found on [developer.onebusaway.org](https://de
### Gradle

```kotlin
implementation("org.onebusaway:onebusaway-sdk-java:0.1.0-alpha.53")
implementation("org.onebusaway:onebusaway-sdk-java:0.1.0-alpha.54")
```

### Maven
Expand All @@ -35,7 +35,7 @@ implementation("org.onebusaway:onebusaway-sdk-java:0.1.0-alpha.53")
<dependency>
<groupId>org.onebusaway</groupId>
<artifactId>onebusaway-sdk-java</artifactId>
<version>0.1.0-alpha.53</version>
<version>0.1.0-alpha.54</version>
</dependency>
```

Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ repositories {

allprojects {
group = "org.onebusaway"
version = "0.1.0-alpha.53" // x-release-please-version
version = "0.1.0-alpha.54" // x-release-please-version
}

subprojects {
Expand Down
2 changes: 0 additions & 2 deletions onebusaway-sdk-java-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ dependencies {
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")

testImplementation(kotlin("test"))
testImplementation(project(":onebusaway-sdk-java-client-okhttp"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ fun getOsName(): String {
}
}

fun getOsVersion(): String = System.getProperty("os.version", "unknown")
fun getOsVersion(): String = System.getProperty("os.version", "unknown") ?: "unknown"

fun getPackageVersion(): String =
OnebusawaySdkClient::class.java.`package`.implementationVersion ?: "unknown"
OnebusawaySdkClient::class.java.`package`?.implementationVersion ?: "unknown"

fun getJavaVersion(): String = System.getProperty("java.version", "unknown")
fun getJavaVersion(): String = System.getProperty("java.version", "unknown") ?: "unknown"
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ 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.ByteArrayInputStream
import java.io.InputStream
import java.io.OutputStream
import java.util.UUID
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.core.toImmutable
import org.onebusaway.errors.OnebusawaySdkInvalidDataException

@JvmSynthetic
Expand All @@ -37,70 +37,208 @@ internal fun multipartFormData(
jsonMapper: JsonMapper,
fields: Map<String, MultipartField<*>>,
): 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<JsonNode>(field.value)
serializePart(name, node)
MultipartBody.Builder()
.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<JsonNode>(field.value)
serializePart(name, node)
}

parts.forEach { (name, bytes) ->
val partBody =
if (bytes is ByteArrayInputStream) {
val byteArray = bytes.readBytes()

object : HttpRequestBody {

override fun writeTo(outputStream: OutputStream) {
outputStream.write(byteArray)
}

override fun contentType(): String = field.contentType

override fun contentLength(): Long = byteArray.size.toLong()

override fun repeatable(): Boolean = true

override fun close() {}
}
} else {
object : HttpRequestBody {

override fun writeTo(outputStream: OutputStream) {
bytes.copyTo(outputStream)
}

override fun contentType(): String = field.contentType

parts.forEach { (name, bytes) ->
addBinaryBody(
name,
bytes,
ContentType.parseLenient(field.contentType),
field.filename().getOrNull(),
)
override fun contentLength(): Long = -1L

override fun repeatable(): Boolean = false

override fun close() = bytes.close()
}
}
}

addPart(
MultipartBody.Part.create(
name,
field.filename().getOrNull(),
field.contentType,
partBody,
)
)
}
.build()
}
}
.build()

private fun serializePart(name: String, node: JsonNode): Sequence<Pair<String, InputStream>> =
when (node.nodeType) {
JsonNodeType.MISSING,
JsonNodeType.NULL -> emptySequence()
JsonNodeType.BINARY -> sequenceOf(name to node.binaryValue().inputStream())
JsonNodeType.STRING -> sequenceOf(name to node.textValue().byteInputStream())
JsonNodeType.BOOLEAN -> sequenceOf(name to node.booleanValue().toString().byteInputStream())
JsonNodeType.NUMBER -> sequenceOf(name to node.numberValue().toString().byteInputStream())
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 serializePart(
name: String,
node: JsonNode,
): Sequence<Pair<String, InputStream>> =
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 class MultipartBody
private constructor(private val boundary: String, private val parts: List<Part>) : HttpRequestBody {
private val boundaryBytes: ByteArray = boundary.toByteArray()
private val contentType = "multipart/form-data; boundary=$boundary"

// This must remain in sync with `contentLength`.
override fun writeTo(outputStream: OutputStream) {
parts.forEach { part ->
outputStream.write(DASHDASH)
outputStream.write(boundaryBytes)
outputStream.write(CRLF)

outputStream.write(CONTENT_DISPOSITION)
outputStream.write(part.contentDisposition.toByteArray())
outputStream.write(CRLF)

outputStream.write(CONTENT_TYPE)
outputStream.write(part.contentType.toByteArray())
outputStream.write(CRLF)

outputStream.write(CRLF)
part.body.writeTo(outputStream)
outputStream.write(CRLF)
}

outputStream.write(DASHDASH)
outputStream.write(boundaryBytes)
outputStream.write(DASHDASH)
outputStream.write(CRLF)
}

override fun contentType(): String = contentType

// This must remain in sync with `writeTo`.
override fun contentLength(): Long {
var byteCount = 0L

parts.forEach { part ->
val contentLength = part.body.contentLength()
if (contentLength == -1L) {
return -1L
}

private fun String.inputStream(): InputStream = toByteArray().inputStream()
byteCount +=
DASHDASH.size +
boundaryBytes.size +
CRLF.size +
CONTENT_DISPOSITION.size +
part.contentDisposition.toByteArray().size +
CRLF.size +
CONTENT_TYPE.size +
part.contentType.toByteArray().size +
CRLF.size +
CRLF.size +
contentLength +
CRLF.size
}

override fun writeTo(outputStream: OutputStream) = entity.writeTo(outputStream)
byteCount += DASHDASH.size + boundaryBytes.size + DASHDASH.size + CRLF.size
return byteCount
}

override fun contentType(): String = entity.contentType
override fun repeatable(): Boolean = parts.all { it.body.repeatable() }

override fun contentLength(): Long = entity.contentLength
override fun close() {
parts.forEach { it.body.close() }
}

override fun repeatable(): Boolean = entity.isRepeatable
class Builder {
private val boundary = UUID.randomUUID().toString()
private val parts: MutableList<Part> = mutableListOf()

override fun close() = entity.close()
fun addPart(part: Part) = apply { parts.add(part) }

fun build() = MultipartBody(boundary, parts.toImmutable())
}

class Part
private constructor(
val contentDisposition: String,
val contentType: String,
val body: HttpRequestBody,
) {
companion object {
fun create(
name: String,
filename: String?,
contentType: String,
body: HttpRequestBody,
): Part {
val disposition = buildString {
append("form-data; name=")
appendQuotedString(name)
if (filename != null) {
append("; filename=")
appendQuotedString(filename)
}
}
return Part(disposition, contentType, body)
}
}
}

companion object {
private val CRLF = byteArrayOf('\r'.code.toByte(), '\n'.code.toByte())
private val DASHDASH = byteArrayOf('-'.code.toByte(), '-'.code.toByte())
private val CONTENT_DISPOSITION = "Content-Disposition: ".toByteArray()
private val CONTENT_TYPE = "Content-Type: ".toByteArray()

private fun StringBuilder.appendQuotedString(key: String) {
append('"')
for (ch in key) {
when (ch) {
'\n' -> append("%0A")
'\r' -> append("%0D")
'"' -> append("%22")
else -> append(ch)
}
}
append('"')
}
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// File generated from our OpenAPI spec by Stainless.

package org.onebusaway.core.http

import java.io.IOException
Expand Down
Loading