@@ -7,13 +7,13 @@ package org.onebusaway.core.http
77import com.fasterxml.jackson.databind.JsonNode
88import com.fasterxml.jackson.databind.json.JsonMapper
99import com.fasterxml.jackson.databind.node.JsonNodeType
10+ import java.io.ByteArrayInputStream
1011import java.io.InputStream
1112import java.io.OutputStream
13+ import java.util.UUID
1214import 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
1615import org.onebusaway.core.MultipartField
16+ import org.onebusaway.core.toImmutable
1717import 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