All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.fireflysource.net.http.client.impl.content.provider.MultiPartContentProvider.kt Maven / Gradle / Ivy

There is a newer version: 5.0.2
Show newest version
package com.fireflysource.net.http.client.impl.content.provider

import com.fireflysource.common.coroutine.event
import com.fireflysource.common.exception.UnsupportedOperationException
import com.fireflysource.common.io.AsyncCloseable
import com.fireflysource.net.http.client.HttpClientContentProvider
import com.fireflysource.net.http.common.model.HttpFields
import com.fireflysource.net.http.common.model.HttpHeader
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.future.await
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets
import java.util.*
import java.util.concurrent.CompletableFuture


class MultiPartContentProvider : HttpClientContentProvider {

    companion object {
        private const val newLine = "\r\n"
        private val colonSpaceBytes: ByteArray = byteArrayOf(':'.toByte(), ' '.toByte())
        private val newLineBytes: ByteArray = byteArrayOf('\r'.toByte(), '\n'.toByte())
    }

    val contentType: String
    private val firstBoundary: ByteArray
    private val middleBoundary: ByteArray
    private val onlyBoundary: ByteArray
    private val lastBoundary: ByteArray
    private val parts: MutableList = LinkedList()

    private var index = 0
    private var state = State.FIRST_BOUNDARY
    private var open = true

    private val multiPartChannel: Channel = Channel(Channel.UNLIMITED)
    private val generatingJob: Job

    init {
        val boundary = makeBoundary()
        this.contentType = "multipart/form-data; boundary=$boundary"

        val firstBoundaryLine = "--$boundary$newLine"
        this.firstBoundary = firstBoundaryLine.toByteArray(StandardCharsets.US_ASCII)

        val middleBoundaryLine = newLine + firstBoundaryLine
        this.middleBoundary = middleBoundaryLine.toByteArray(StandardCharsets.US_ASCII)

        val onlyBoundaryLine = "--$boundary--$newLine"
        this.onlyBoundary = onlyBoundaryLine.toByteArray(StandardCharsets.US_ASCII)

        val lastBoundaryLine = newLine + onlyBoundaryLine
        this.lastBoundary = lastBoundaryLine.toByteArray(StandardCharsets.US_ASCII)

        generatingJob = event {
            readMessageLoop@ while (true) {
                when (val readMultiPartMessage = multiPartChannel.receive()) {
                    is GenerateMultiPart -> {
                        val (buf, future) = readMultiPartMessage
                        try {
                            val len = generate(buf)
                            future.complete(len)
                        } catch (e: Exception) {
                            future.completeExceptionally(e)
                        }
                    }
                    is EndMultiPartProvider -> {
                        open = false
                        state = State.COMPLETE
                        parts.forEach { part -> part.closeAsync().await() }
                        break@readMessageLoop
                    }
                }

            }
        }
    }

    /**
     * 

Adds a field part with the given {@code name} as field name, and the given * {@code content} as part content.

* * @param name the part name * @param content the part content * @param fields the headers associated with this part */ fun addPart(name: String, content: HttpClientContentProvider, fields: HttpFields?) { parts.add(Part(name, null, content, fields, "text/plain")) } /** *

Adds a file part with the given {@code name} as field name, the given * {@code fileName} as file name, and the given {@code content} as part content.

* * @param name the part name * @param fileName the file name associated to this part * @param content the part content * @param fields the headers associated with this part */ fun addFilePart(name: String, fileName: String?, content: HttpClientContentProvider, fields: HttpFields?) { parts.add(Part(name, fileName, content, fields, "application/octet-stream")) } override fun length(): Long { // Compute the length, if possible. if (parts.isEmpty()) { return onlyBoundary.size.toLong() } else { var result: Long = 0 for (i in 0 until parts.size) { result += if (i == 0) firstBoundary.size.toLong() else middleBoundary.size.toLong() val part = parts[i] val partLength = part.length result += partLength if (partLength < 0) { result = -1 break } } if (result > 0) { result += lastBoundary.size.toLong() } return result } } override fun isOpen(): Boolean = open override fun toByteBuffer(): ByteBuffer { throw UnsupportedOperationException("The multi part content does not support this method") } private suspend fun closeAwait() { close() generatingJob.join() } override fun closeAsync(): CompletableFuture { val future = CompletableFuture() event { closeAwait() future.complete(null) } return future } override fun close() { multiPartChannel.offer(EndMultiPartProvider) } override fun read(byteBuffer: ByteBuffer): CompletableFuture { if (!isOpen) { return endStream() } if (state == State.COMPLETE) { return endStream() } val future = CompletableFuture() multiPartChannel.offer(GenerateMultiPart(byteBuffer, future)) return future } private suspend fun generate(byteBuffer: ByteBuffer): Int { while (true) { when (state) { State.FIRST_BOUNDARY -> { return if (parts.isEmpty()) { state = State.COMPLETE byteBuffer.put(onlyBoundary) onlyBoundary.size } else { state = State.HEADERS byteBuffer.put(firstBoundary) firstBoundary.size } } State.HEADERS -> { val part = parts[index] state = State.CONTENT byteBuffer.put(part.headers) return part.headers.size } State.CONTENT -> { val part = parts[index] val len = part.content.read(byteBuffer).await() if (len >= 0) { return len } else { ++index state = if (index == parts.size) State.LAST_BOUNDARY else State.MIDDLE_BOUNDARY } } State.MIDDLE_BOUNDARY -> { state = State.HEADERS byteBuffer.put(middleBoundary) return middleBoundary.size } State.LAST_BOUNDARY -> { state = State.COMPLETE byteBuffer.put(lastBoundary) return lastBoundary.size } State.COMPLETE -> { open = false return -1 } } } } private fun makeBoundary(): String { val random = Random() val builder = StringBuilder("FireflyHttpClientBoundary") val length = builder.length while (builder.length < length + 16) { val rnd = random.nextLong() builder.append((if (rnd < 0) -rnd else rnd).toString(36)) } builder.setLength(length + 16) return builder.toString() } private class Part( name: String, fileName: String?, val content: HttpClientContentProvider, fields: HttpFields?, val contentType: String ) : AsyncCloseable { val headers: ByteArray val length: Long init { // Compute the Content-Disposition. var contentDisposition = "Content-Disposition: form-data; name=\"$name\"" if (fileName != null) { contentDisposition += "; filename=\"$fileName\"" } contentDisposition += newLine // Compute the Content-Type. var contentType = fields?.get(HttpHeader.CONTENT_TYPE) if (contentType == null) { contentType = this.contentType } contentType = "Content-Type: $contentType$newLine" // Compute the headers if (fields == null || fields.size() == 0) { var headers = contentDisposition headers += contentType headers += newLine this.headers = headers.toByteArray(StandardCharsets.UTF_8) } else { val buffer = ByteArrayOutputStream((fields.size() + 1) * contentDisposition.length) buffer.write(contentDisposition.toByteArray(StandardCharsets.UTF_8)) buffer.write(contentType.toByteArray(StandardCharsets.UTF_8)) for (field in fields) { if (HttpHeader.CONTENT_TYPE == field.header) { continue } buffer.write(field.name.toByteArray(StandardCharsets.US_ASCII)) buffer.write(colonSpaceBytes) val value = field.value if (value != null) { buffer.write(value.toByteArray(StandardCharsets.UTF_8)) } buffer.write(newLineBytes) } buffer.write(newLineBytes) headers = buffer.toByteArray() } length = if (content.length() >= 0) headers.size + content.length() else -1 } override fun closeAsync(): CompletableFuture { return content.closeAsync() } override fun close() { content.close() } } private enum class State { FIRST_BOUNDARY, HEADERS, CONTENT, MIDDLE_BOUNDARY, LAST_BOUNDARY, COMPLETE } } sealed class MultiPartProviderMessage data class GenerateMultiPart( val byteBuffer: ByteBuffer, val future: CompletableFuture ) : MultiPartProviderMessage() object EndMultiPartProvider : MultiPartProviderMessage()




© 2015 - 2024 Weber Informatics LLC | Privacy Policy