Skip to content

Commit edcdb51

Browse files
chore: drop apache dependency
1 parent 39c553f commit edcdb51

File tree

3 files changed

+932
-57
lines changed

3 files changed

+932
-57
lines changed

orb-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(":orb-java-client-okhttp"))

orb-java-core/src/main/kotlin/com/withorb/api/core/http/HttpRequestBodies.kt

Lines changed: 193 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ import com.fasterxml.jackson.databind.JsonNode
88
import com.fasterxml.jackson.databind.json.JsonMapper
99
import com.fasterxml.jackson.databind.node.JsonNodeType
1010
import com.withorb.api.core.MultipartField
11+
import com.withorb.api.core.toImmutable
1112
import com.withorb.api.errors.OrbInvalidDataException
13+
import java.io.ByteArrayInputStream
1214
import java.io.InputStream
1315
import java.io.OutputStream
16+
import java.util.UUID
1417
import kotlin.jvm.optionals.getOrNull
15-
import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder
16-
import org.apache.hc.core5.http.ContentType
17-
import org.apache.hc.core5.http.HttpEntity
1818

1919
@JvmSynthetic
2020
internal inline fun <reified T> json(jsonMapper: JsonMapper, value: T): HttpRequestBody =
@@ -37,69 +37,207 @@ 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 -> throw OrbInvalidDataException("Unexpected JsonNode type: ${node.nodeType}")
119+
}
68120

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 ->
84-
serializePart("$name[]", element)
85-
}
86-
JsonNodeType.OBJECT ->
87-
node.fields().asSequence().flatMap { (key, value) ->
88-
serializePart("$name[$key]", value)
89-
}
90-
JsonNodeType.POJO,
91-
null -> throw OrbInvalidDataException("Unexpected JsonNode type: ${node.nodeType}")
121+
private class MultipartBody
122+
private constructor(private val boundary: String, private val parts: List<Part>) : HttpRequestBody {
123+
private val boundaryBytes: ByteArray = boundary.toByteArray()
124+
private val contentType = "multipart/form-data; boundary=$boundary"
125+
126+
// This must remain in sync with `contentLength`.
127+
override fun writeTo(outputStream: OutputStream) {
128+
parts.forEach { part ->
129+
outputStream.write(DASHDASH)
130+
outputStream.write(boundaryBytes)
131+
outputStream.write(CRLF)
132+
133+
outputStream.write(CONTENT_DISPOSITION)
134+
outputStream.write(part.contentDisposition.toByteArray())
135+
outputStream.write(CRLF)
136+
137+
outputStream.write(CONTENT_TYPE)
138+
outputStream.write(part.contentType.toByteArray())
139+
outputStream.write(CRLF)
140+
141+
outputStream.write(CRLF)
142+
part.body.writeTo(outputStream)
143+
outputStream.write(CRLF)
144+
}
145+
146+
outputStream.write(DASHDASH)
147+
outputStream.write(boundaryBytes)
148+
outputStream.write(DASHDASH)
149+
outputStream.write(CRLF)
150+
}
151+
152+
override fun contentType(): String = contentType
153+
154+
// This must remain in sync with `writeTo`.
155+
override fun contentLength(): Long {
156+
var byteCount = 0L
157+
158+
parts.forEach { part ->
159+
val contentLength = part.body.contentLength()
160+
if (contentLength == -1L) {
161+
return -1L
92162
}
93163

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

96-
override fun writeTo(outputStream: OutputStream) = entity.writeTo(outputStream)
179+
byteCount += DASHDASH.size + boundaryBytes.size + DASHDASH.size + CRLF.size
180+
return byteCount
181+
}
97182

98-
override fun contentType(): String = entity.contentType
183+
override fun repeatable(): Boolean = parts.all { it.body.repeatable() }
99184

100-
override fun contentLength(): Long = entity.contentLength
185+
override fun close() {
186+
parts.forEach { it.body.close() }
187+
}
101188

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

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

0 commit comments

Comments
 (0)