Skip to content

Commit 7c4dc22

Browse files
chore: drop apache dependency
1 parent a743178 commit 7c4dc22

3 files changed

Lines changed: 933 additions & 58 deletions

File tree

onebusaway-sdk-java-core/build.gradle.kts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@ dependencies {
2727
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.18.2")
2828
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2")
2929
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.18.2")
30-
implementation("org.apache.httpcomponents.core5:httpcore5:5.2.4")
31-
implementation("org.apache.httpcomponents.client5:httpclient5:5.3.1")
3230

3331
testImplementation(kotlin("test"))
3432
testImplementation(project(":onebusaway-sdk-java-client-okhttp"))

onebusaway-sdk-java-core/src/main/kotlin/org/onebusaway/core/http/HttpRequestBodies.kt

Lines changed: 194 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ package org.onebusaway.core.http
77
import com.fasterxml.jackson.databind.JsonNode
88
import com.fasterxml.jackson.databind.json.JsonMapper
99
import com.fasterxml.jackson.databind.node.JsonNodeType
10+
import java.io.ByteArrayInputStream
1011
import java.io.InputStream
1112
import java.io.OutputStream
13+
import java.util.UUID
1214
import kotlin.jvm.optionals.getOrNull
13-
import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder
14-
import org.apache.hc.core5.http.ContentType
15-
import org.apache.hc.core5.http.HttpEntity
1615
import org.onebusaway.core.MultipartField
16+
import org.onebusaway.core.toImmutable
1717
import org.onebusaway.errors.OnebusawaySdkInvalidDataException
1818

1919
@JvmSynthetic
@@ -37,70 +37,208 @@ internal fun multipartFormData(
3737
jsonMapper: JsonMapper,
3838
fields: Map<String, MultipartField<*>>,
3939
): HttpRequestBody =
40-
object : HttpRequestBody {
41-
private val entity: HttpEntity by lazy {
42-
MultipartEntityBuilder.create()
43-
.apply {
44-
fields.forEach { (name, field) ->
45-
val knownValue = field.value.asKnown().getOrNull()
46-
val parts =
47-
if (knownValue is InputStream) {
48-
// Read directly from the `InputStream` instead of reading it all
49-
// into memory due to the `jsonMapper` serialization below.
50-
sequenceOf(name to knownValue)
51-
} else {
52-
val node = jsonMapper.valueToTree<JsonNode>(field.value)
53-
serializePart(name, node)
40+
MultipartBody.Builder()
41+
.apply {
42+
fields.forEach { (name, field) ->
43+
val knownValue = field.value.asKnown().getOrNull()
44+
val parts =
45+
if (knownValue is InputStream) {
46+
// Read directly from the `InputStream` instead of reading it all
47+
// into memory due to the `jsonMapper` serialization below.
48+
sequenceOf(name to knownValue)
49+
} else {
50+
val node = jsonMapper.valueToTree<JsonNode>(field.value)
51+
serializePart(name, node)
52+
}
53+
54+
parts.forEach { (name, bytes) ->
55+
val partBody =
56+
if (bytes is ByteArrayInputStream) {
57+
val byteArray = bytes.readBytes()
58+
59+
object : HttpRequestBody {
60+
61+
override fun writeTo(outputStream: OutputStream) {
62+
outputStream.write(byteArray)
63+
}
64+
65+
override fun contentType(): String = field.contentType
66+
67+
override fun contentLength(): Long = byteArray.size.toLong()
68+
69+
override fun repeatable(): Boolean = true
70+
71+
override fun close() {}
5472
}
73+
} else {
74+
object : HttpRequestBody {
75+
76+
override fun writeTo(outputStream: OutputStream) {
77+
bytes.copyTo(outputStream)
78+
}
79+
80+
override fun contentType(): String = field.contentType
5581

56-
parts.forEach { (name, bytes) ->
57-
addBinaryBody(
58-
name,
59-
bytes,
60-
ContentType.parseLenient(field.contentType),
61-
field.filename().getOrNull(),
62-
)
82+
override fun contentLength(): Long = -1L
83+
84+
override fun repeatable(): Boolean = false
85+
86+
override fun close() = bytes.close()
87+
}
6388
}
64-
}
89+
90+
addPart(
91+
MultipartBody.Part.create(
92+
name,
93+
field.filename().getOrNull(),
94+
field.contentType,
95+
partBody,
96+
)
97+
)
6598
}
66-
.build()
99+
}
67100
}
101+
.build()
102+
103+
private fun serializePart(name: String, node: JsonNode): Sequence<Pair<String, InputStream>> =
104+
when (node.nodeType) {
105+
JsonNodeType.MISSING,
106+
JsonNodeType.NULL -> emptySequence()
107+
JsonNodeType.BINARY -> sequenceOf(name to node.binaryValue().inputStream())
108+
JsonNodeType.STRING -> sequenceOf(name to node.textValue().byteInputStream())
109+
JsonNodeType.BOOLEAN -> sequenceOf(name to node.booleanValue().toString().byteInputStream())
110+
JsonNodeType.NUMBER -> sequenceOf(name to node.numberValue().toString().byteInputStream())
111+
JsonNodeType.ARRAY ->
112+
node.elements().asSequence().flatMap { element -> serializePart(name, element) }
113+
JsonNodeType.OBJECT ->
114+
node.fields().asSequence().flatMap { (key, value) ->
115+
serializePart("$name[$key]", value)
116+
}
117+
JsonNodeType.POJO,
118+
null ->
119+
throw OnebusawaySdkInvalidDataException("Unexpected JsonNode type: ${node.nodeType}")
120+
}
68121

69-
private fun serializePart(
70-
name: String,
71-
node: JsonNode,
72-
): Sequence<Pair<String, InputStream>> =
73-
when (node.nodeType) {
74-
JsonNodeType.MISSING,
75-
JsonNodeType.NULL -> emptySequence()
76-
JsonNodeType.BINARY -> sequenceOf(name to node.binaryValue().inputStream())
77-
JsonNodeType.STRING -> sequenceOf(name to node.textValue().inputStream())
78-
JsonNodeType.BOOLEAN ->
79-
sequenceOf(name to node.booleanValue().toString().inputStream())
80-
JsonNodeType.NUMBER ->
81-
sequenceOf(name to node.numberValue().toString().inputStream())
82-
JsonNodeType.ARRAY ->
83-
node.elements().asSequence().flatMap { element -> serializePart(name, element) }
84-
JsonNodeType.OBJECT ->
85-
node.fields().asSequence().flatMap { (key, value) ->
86-
serializePart("$name[$key]", value)
87-
}
88-
JsonNodeType.POJO,
89-
null ->
90-
throw OnebusawaySdkInvalidDataException(
91-
"Unexpected JsonNode type: ${node.nodeType}"
92-
)
122+
private class MultipartBody
123+
private constructor(private val boundary: String, private val parts: List<Part>) : HttpRequestBody {
124+
private val boundaryBytes: ByteArray = boundary.toByteArray()
125+
private val contentType = "multipart/form-data; boundary=$boundary"
126+
127+
// This must remain in sync with `contentLength`.
128+
override fun writeTo(outputStream: OutputStream) {
129+
parts.forEach { part ->
130+
outputStream.write(DASHDASH)
131+
outputStream.write(boundaryBytes)
132+
outputStream.write(CRLF)
133+
134+
outputStream.write(CONTENT_DISPOSITION)
135+
outputStream.write(part.contentDisposition.toByteArray())
136+
outputStream.write(CRLF)
137+
138+
outputStream.write(CONTENT_TYPE)
139+
outputStream.write(part.contentType.toByteArray())
140+
outputStream.write(CRLF)
141+
142+
outputStream.write(CRLF)
143+
part.body.writeTo(outputStream)
144+
outputStream.write(CRLF)
145+
}
146+
147+
outputStream.write(DASHDASH)
148+
outputStream.write(boundaryBytes)
149+
outputStream.write(DASHDASH)
150+
outputStream.write(CRLF)
151+
}
152+
153+
override fun contentType(): String = contentType
154+
155+
// This must remain in sync with `writeTo`.
156+
override fun contentLength(): Long {
157+
var byteCount = 0L
158+
159+
parts.forEach { part ->
160+
val contentLength = part.body.contentLength()
161+
if (contentLength == -1L) {
162+
return -1L
93163
}
94164

95-
private fun String.inputStream(): InputStream = toByteArray().inputStream()
165+
byteCount +=
166+
DASHDASH.size +
167+
boundaryBytes.size +
168+
CRLF.size +
169+
CONTENT_DISPOSITION.size +
170+
part.contentDisposition.toByteArray().size +
171+
CRLF.size +
172+
CONTENT_TYPE.size +
173+
part.contentType.toByteArray().size +
174+
CRLF.size +
175+
CRLF.size +
176+
contentLength +
177+
CRLF.size
178+
}
96179

97-
override fun writeTo(outputStream: OutputStream) = entity.writeTo(outputStream)
180+
byteCount += DASHDASH.size + boundaryBytes.size + DASHDASH.size + CRLF.size
181+
return byteCount
182+
}
98183

99-
override fun contentType(): String = entity.contentType
184+
override fun repeatable(): Boolean = parts.all { it.body.repeatable() }
100185

101-
override fun contentLength(): Long = entity.contentLength
186+
override fun close() {
187+
parts.forEach { it.body.close() }
188+
}
102189

103-
override fun repeatable(): Boolean = entity.isRepeatable
190+
class Builder {
191+
private val boundary = UUID.randomUUID().toString()
192+
private val parts: MutableList<Part> = mutableListOf()
104193

105-
override fun close() = entity.close()
194+
fun addPart(part: Part) = apply { parts.add(part) }
195+
196+
fun build() = MultipartBody(boundary, parts.toImmutable())
197+
}
198+
199+
class Part
200+
private constructor(
201+
val contentDisposition: String,
202+
val contentType: String,
203+
val body: HttpRequestBody,
204+
) {
205+
companion object {
206+
fun create(
207+
name: String,
208+
filename: String?,
209+
contentType: String,
210+
body: HttpRequestBody,
211+
): Part {
212+
val disposition = buildString {
213+
append("form-data; name=")
214+
appendQuotedString(name)
215+
if (filename != null) {
216+
append("; filename=")
217+
appendQuotedString(filename)
218+
}
219+
}
220+
return Part(disposition, contentType, body)
221+
}
222+
}
223+
}
224+
225+
companion object {
226+
private val CRLF = byteArrayOf('\r'.code.toByte(), '\n'.code.toByte())
227+
private val DASHDASH = byteArrayOf('-'.code.toByte(), '-'.code.toByte())
228+
private val CONTENT_DISPOSITION = "Content-Disposition: ".toByteArray()
229+
private val CONTENT_TYPE = "Content-Type: ".toByteArray()
230+
231+
private fun StringBuilder.appendQuotedString(key: String) {
232+
append('"')
233+
for (ch in key) {
234+
when (ch) {
235+
'\n' -> append("%0A")
236+
'\r' -> append("%0D")
237+
'"' -> append("%22")
238+
else -> append(ch)
239+
}
240+
}
241+
append('"')
242+
}
106243
}
244+
}

0 commit comments

Comments
 (0)