Skip to content

Commit d693eb2

Browse files
chore: drop apache dependency
1 parent 8f82431 commit d693eb2

File tree

3 files changed

+946
-80
lines changed

3 files changed

+946
-80
lines changed

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

brand-dev-java-core/src/main/kotlin/com/branddev/api/core/http/HttpRequestBodies.kt

Lines changed: 217 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@
55
package com.branddev.api.core.http
66

77
import com.branddev.api.core.MultipartField
8+
import com.branddev.api.core.toImmutable
89
import com.branddev.api.errors.BrandDevInvalidDataException
910
import com.fasterxml.jackson.databind.JsonNode
1011
import com.fasterxml.jackson.databind.json.JsonMapper
1112
import com.fasterxml.jackson.databind.node.JsonNodeType
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,92 +37,231 @@ 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
81+
82+
override fun contentLength(): Long = -1L
5583

56-
parts.forEach { (name, bytes) ->
57-
addBinaryBody(
58-
name,
59-
bytes,
60-
ContentType.parseLenient(field.contentType),
61-
field.filename().getOrNull(),
62-
)
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()
68102

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-
sequenceOf(
84-
name to
85-
node
86-
.elements()
87-
.asSequence()
88-
.mapNotNull { element ->
89-
when (element.nodeType) {
90-
JsonNodeType.MISSING,
91-
JsonNodeType.NULL -> null
92-
JsonNodeType.STRING -> node.textValue()
93-
JsonNodeType.BOOLEAN -> node.booleanValue().toString()
94-
JsonNodeType.NUMBER -> node.numberValue().toString()
95-
null,
96-
JsonNodeType.BINARY,
97-
JsonNodeType.ARRAY,
98-
JsonNodeType.OBJECT,
99-
JsonNodeType.POJO ->
100-
throw BrandDevInvalidDataException(
101-
"Unexpected JsonNode type in array: ${node.nodeType}"
102-
)
103-
}
104-
}
105-
.joinToString(",")
106-
.inputStream()
107-
)
108-
JsonNodeType.OBJECT ->
109-
node.fields().asSequence().flatMap { (key, value) ->
110-
serializePart("$name[$key]", value)
111-
}
112-
JsonNodeType.POJO,
113-
null ->
114-
throw BrandDevInvalidDataException("Unexpected JsonNode type: ${node.nodeType}")
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+
sequenceOf(
113+
name to
114+
node
115+
.elements()
116+
.asSequence()
117+
.mapNotNull { element ->
118+
when (element.nodeType) {
119+
JsonNodeType.MISSING,
120+
JsonNodeType.NULL -> null
121+
JsonNodeType.STRING -> element.textValue()
122+
JsonNodeType.BOOLEAN -> element.booleanValue().toString()
123+
JsonNodeType.NUMBER -> element.numberValue().toString()
124+
null,
125+
JsonNodeType.BINARY,
126+
JsonNodeType.ARRAY,
127+
JsonNodeType.OBJECT,
128+
JsonNodeType.POJO ->
129+
throw BrandDevInvalidDataException(
130+
"Unexpected JsonNode type in array: ${element.nodeType}"
131+
)
132+
}
133+
}
134+
.joinToString(",")
135+
.byteInputStream()
136+
)
137+
JsonNodeType.OBJECT ->
138+
node.fields().asSequence().flatMap { (key, value) ->
139+
serializePart("$name[$key]", value)
140+
}
141+
JsonNodeType.POJO,
142+
null -> throw BrandDevInvalidDataException("Unexpected JsonNode type: ${node.nodeType}")
143+
}
144+
145+
private class MultipartBody
146+
private constructor(private val boundary: String, private val parts: List<Part>) : HttpRequestBody {
147+
private val boundaryBytes: ByteArray = boundary.toByteArray()
148+
private val contentType = "multipart/form-data; boundary=$boundary"
149+
150+
// This must remain in sync with `contentLength`.
151+
override fun writeTo(outputStream: OutputStream) {
152+
parts.forEach { part ->
153+
outputStream.write(DASHDASH)
154+
outputStream.write(boundaryBytes)
155+
outputStream.write(CRLF)
156+
157+
outputStream.write(CONTENT_DISPOSITION)
158+
outputStream.write(part.contentDisposition.toByteArray())
159+
outputStream.write(CRLF)
160+
161+
outputStream.write(CONTENT_TYPE)
162+
outputStream.write(part.contentType.toByteArray())
163+
outputStream.write(CRLF)
164+
165+
outputStream.write(CRLF)
166+
part.body.writeTo(outputStream)
167+
outputStream.write(CRLF)
168+
}
169+
170+
outputStream.write(DASHDASH)
171+
outputStream.write(boundaryBytes)
172+
outputStream.write(DASHDASH)
173+
outputStream.write(CRLF)
174+
}
175+
176+
override fun contentType(): String = contentType
177+
178+
// This must remain in sync with `writeTo`.
179+
override fun contentLength(): Long {
180+
var byteCount = 0L
181+
182+
parts.forEach { part ->
183+
val contentLength = part.body.contentLength()
184+
if (contentLength == -1L) {
185+
return -1L
115186
}
116187

117-
private fun String.inputStream(): InputStream = toByteArray().inputStream()
188+
byteCount +=
189+
DASHDASH.size +
190+
boundaryBytes.size +
191+
CRLF.size +
192+
CONTENT_DISPOSITION.size +
193+
part.contentDisposition.toByteArray().size +
194+
CRLF.size +
195+
CONTENT_TYPE.size +
196+
part.contentType.toByteArray().size +
197+
CRLF.size +
198+
CRLF.size +
199+
contentLength +
200+
CRLF.size
201+
}
118202

119-
override fun writeTo(outputStream: OutputStream) = entity.writeTo(outputStream)
203+
byteCount += DASHDASH.size + boundaryBytes.size + DASHDASH.size + CRLF.size
204+
return byteCount
205+
}
120206

121-
override fun contentType(): String = entity.contentType
207+
override fun repeatable(): Boolean = parts.all { it.body.repeatable() }
122208

123-
override fun contentLength(): Long = entity.contentLength
209+
override fun close() {
210+
parts.forEach { it.body.close() }
211+
}
124212

125-
override fun repeatable(): Boolean = entity.isRepeatable
213+
class Builder {
214+
private val boundary = UUID.randomUUID().toString()
215+
private val parts: MutableList<Part> = mutableListOf()
126216

127-
override fun close() = entity.close()
217+
fun addPart(part: Part) = apply { parts.add(part) }
218+
219+
fun build() = MultipartBody(boundary, parts.toImmutable())
220+
}
221+
222+
class Part
223+
private constructor(
224+
val contentDisposition: String,
225+
val contentType: String,
226+
val body: HttpRequestBody,
227+
) {
228+
companion object {
229+
fun create(
230+
name: String,
231+
filename: String?,
232+
contentType: String,
233+
body: HttpRequestBody,
234+
): Part {
235+
val disposition = buildString {
236+
append("form-data; name=")
237+
appendQuotedString(name)
238+
if (filename != null) {
239+
append("; filename=")
240+
appendQuotedString(filename)
241+
}
242+
}
243+
return Part(disposition, contentType, body)
244+
}
245+
}
246+
}
247+
248+
companion object {
249+
private val CRLF = byteArrayOf('\r'.code.toByte(), '\n'.code.toByte())
250+
private val DASHDASH = byteArrayOf('-'.code.toByte(), '-'.code.toByte())
251+
private val CONTENT_DISPOSITION = "Content-Disposition: ".toByteArray()
252+
private val CONTENT_TYPE = "Content-Type: ".toByteArray()
253+
254+
private fun StringBuilder.appendQuotedString(key: String) {
255+
append('"')
256+
for (ch in key) {
257+
when (ch) {
258+
'\n' -> append("%0A")
259+
'\r' -> append("%0D")
260+
'"' -> append("%22")
261+
else -> append(ch)
262+
}
263+
}
264+
append('"')
265+
}
128266
}
267+
}

0 commit comments

Comments
 (0)