import ballerina/http;
import ballerina/io;
import ballerina/log;
import ballerina/mime;

// Creates an endpoint for the client.
http:Client clientEP = check new ("http://localhost:9092");

service /multiparts on new http:Listener(9092) {

    resource function get encode_out_response() returns http:Response {
        // Creates an enclosing entity to hold the child parts.
        mime:Entity parentPart = new;

        // Creates a child part with the JSON content.
        mime:Entity childPart1 = new;
        childPart1.setJson({"name": "wso2"});
        // Creates another child part with a file.
        mime:Entity childPart2 = new;
        // This file path is relative to where the Ballerina is running.
        //If your file is located outside, please give the
        //absolute file path instead.
        childPart2.setFileAsEntityBody("./files/test.xml",
            contentType = mime:TEXT_XML);
        // Creates an array to hold the child parts.
        mime:Entity[] childParts = [childPart1, childPart2];
        // [Sets the child parts to the parent part](https://docs.central.ballerina.io/ballerina/mime/latest/classes/Entity#setBodyParts).
        parentPart.setBodyParts(childParts,
            contentType = mime:MULTIPART_MIXED);
        // Creates an array to hold the parent part and set it to the response.
        mime:Entity[] immediatePartsToResponse = [parentPart];
        http:Response outResponse = new;
        outResponse.setBodyParts(immediatePartsToResponse,
            contentType = mime:MULTIPART_FORM_DATA);
        return outResponse;
    }
}

// Binds the listener to the service.
service /multiparts on new http:Listener(9090) {

    // This resource accepts multipart responses.
    resource function get decode_in_response()
            returns string|http:InternalServerError {
        http:Response|error returnResult = clientEP->get(
                        "/multiparts/encode_out_response");
        if (returnResult is http:Response) {
            // [Extracts the body parts](https://docs.central.ballerina.io/ballerina/http/latest/classes/Response#getBodyParts)  from the response.
            var parentParts = returnResult.getBodyParts();
            if (parentParts is mime:Entity[]) {
                //Loops through body parts.
                foreach var parentPart in parentParts {
                    handleNestedParts(parentPart);
                }
                return "Body Parts Received!";
            } else {
                return { body: "Invalid payload"};
            }
        } else {
            return { body: "Connection error"};
        }
    }
}

// Gets the child parts that are nested within the parent.
function handleNestedParts(mime:Entity parentPart) {
    string contentTypeOfParent = parentPart.getContentType();
    if (contentTypeOfParent.startsWith("multipart/")) {
        var childParts = parentPart.getBodyParts();
        if (childParts is mime:Entity[]) {
            log:printInfo("Nested Parts Detected!");
            foreach var childPart in childParts {
                handleContent(childPart);
            }
        } else {
            log:printError("Error retrieving child parts! " +
                            childParts.message());
        }
    }
}

//The content logic that handles the body parts
//vary based on your requirement.
function handleContent(mime:Entity bodyPart) {
    string baseType = getBaseType(bodyPart.getContentType());
    if (mime:APPLICATION_XML == baseType || mime:TEXT_XML == baseType) {
        // [Extracts XML data](https://docs.central.ballerina.io/ballerina/mime/latest/classes/Entity#getXml) from the body part.
        var payload = bodyPart.getXml();
        if (payload is xml) {
             log:printInfo("XML data: " + payload.toString());
        } else {
             log:printError("Error in parsing XML data", 'error = payload);
        }
    } else if (mime:APPLICATION_JSON == baseType) {
        // [Extracts JSON data](https://docs.central.ballerina.io/ballerina/mime/latest/classes/Entity#getJson) from the body part.
        var payload = bodyPart.getJson();
        if (payload is json) {
            log:printInfo("JSON data: " + payload.toJsonString());
        } else {
             log:printError("Error in parsing JSON data", 'error = payload);
        }
    } else if (mime:TEXT_PLAIN == baseType) {
        // [Extracts text data](https://docs.central.ballerina.io/ballerina/mime/latest/classes/Entity#getText) from the body part.
        var payload = bodyPart.getText();
        if (payload is string) {
            log:printInfo("Text data: " + payload);
        } else {
            log:printError("Error in parsing text data", 'error = payload);
        }
    } else if (mime:APPLICATION_PDF == baseType) {
        // [Extracts the byte stream](https://docs.central.ballerina.io/ballerina/http/latest/classes/Response#getByteStream) from the body part and saves it as a file.
        var payload = bodyPart.getByteStream();
        if (payload is stream<byte[], io:Error?>) {
            //Writes the incoming stream to a file using `io:fileWriteBlocksFromStream` API by providing the file location to which the content should be written to.
            io:Error? result = io:fileWriteBlocksFromStream(
                                    "./files/ReceivedFile.pdf", payload);

            if (result is error) {
                log:printError("Error occurred while writing ",
                                'error = result);
            }
            close(payload);
        } else {
            log:printError("Error in parsing byte channel :",
                            'error = payload);
        }
    }
}

//Gets the base type from a given content type.
function getBaseType(string contentType) returns string {
    var result = mime:getMediaType(contentType);
    if (result is mime:MediaType) {
        return result.getBaseType();
    } else {
        panic result;
    }
}

//Closes the byte stream.
function close(stream<byte[], io:Error?> byteStream) {
    var cr = byteStream.close();
    if (cr is error) {
        log:printError("Error occurred while closing the stream: ",
                       'error = cr);
    }
}

Response With Multiparts

Ballerina supports encoding and decoding multipart content in HTTP responses along with the nested parts. When you request multiparts from an HTTP inbound response, you get an array of the parts of the body (an array of entities). If the received parts contain nested parts, you can loop through the parent parts and get the child parts.

For more information on the underlying module, see the Mime module.

import ballerina/http;
import ballerina/io;
import ballerina/log;
import ballerina/mime;
http:Client clientEP = check new ("http://localhost:9092");

Creates an endpoint for the client.

service /multiparts on new http:Listener(9092) {
    resource function get encode_out_response() returns http:Response {
        mime:Entity parentPart = new;

Creates an enclosing entity to hold the child parts.

        mime:Entity childPart1 = new;
        childPart1.setJson({"name": "wso2"});

Creates a child part with the JSON content.

        mime:Entity childPart2 = new;

Creates another child part with a file.

        childPart2.setFileAsEntityBody("./files/test.xml",
            contentType = mime:TEXT_XML);

This file path is relative to where the Ballerina is running. If your file is located outside, please give the absolute file path instead.

        mime:Entity[] childParts = [childPart1, childPart2];

Creates an array to hold the child parts.

        parentPart.setBodyParts(childParts,
            contentType = mime:MULTIPART_MIXED);
        mime:Entity[] immediatePartsToResponse = [parentPart];
        http:Response outResponse = new;
        outResponse.setBodyParts(immediatePartsToResponse,
            contentType = mime:MULTIPART_FORM_DATA);
        return outResponse;
    }
}

Creates an array to hold the parent part and set it to the response.

service /multiparts on new http:Listener(9090) {

Binds the listener to the service.

    resource function get decode_in_response()
            returns string|http:InternalServerError {
        http:Response|error returnResult = clientEP->get(
                        "/multiparts/encode_out_response");
        if (returnResult is http:Response) {

This resource accepts multipart responses.

            var parentParts = returnResult.getBodyParts();
            if (parentParts is mime:Entity[]) {

Extracts the body parts from the response.

                foreach var parentPart in parentParts {
                    handleNestedParts(parentPart);
                }
                return "Body Parts Received!";
            } else {
                return { body: "Invalid payload"};
            }
        } else {
            return { body: "Connection error"};
        }
    }
}

Loops through body parts.

function handleNestedParts(mime:Entity parentPart) {
    string contentTypeOfParent = parentPart.getContentType();
    if (contentTypeOfParent.startsWith("multipart/")) {
        var childParts = parentPart.getBodyParts();
        if (childParts is mime:Entity[]) {
            log:printInfo("Nested Parts Detected!");
            foreach var childPart in childParts {
                handleContent(childPart);
            }
        } else {
            log:printError("Error retrieving child parts! " +
                            childParts.message());
        }
    }
}

Gets the child parts that are nested within the parent.

function handleContent(mime:Entity bodyPart) {
    string baseType = getBaseType(bodyPart.getContentType());
    if (mime:APPLICATION_XML == baseType || mime:TEXT_XML == baseType) {

The content logic that handles the body parts vary based on your requirement.

        var payload = bodyPart.getXml();
        if (payload is xml) {
             log:printInfo("XML data: " + payload.toString());
        } else {
             log:printError("Error in parsing XML data", 'error = payload);
        }
    } else if (mime:APPLICATION_JSON == baseType) {

Extracts XML data from the body part.

        var payload = bodyPart.getJson();
        if (payload is json) {
            log:printInfo("JSON data: " + payload.toJsonString());
        } else {
             log:printError("Error in parsing JSON data", 'error = payload);
        }
    } else if (mime:TEXT_PLAIN == baseType) {

Extracts JSON data from the body part.

        var payload = bodyPart.getText();
        if (payload is string) {
            log:printInfo("Text data: " + payload);
        } else {
            log:printError("Error in parsing text data", 'error = payload);
        }
    } else if (mime:APPLICATION_PDF == baseType) {

Extracts text data from the body part.

        var payload = bodyPart.getByteStream();
        if (payload is stream<byte[], io:Error?>) {

Extracts the byte stream from the body part and saves it as a file.

            io:Error? result = io:fileWriteBlocksFromStream(
                                    "./files/ReceivedFile.pdf", payload);

Writes the incoming stream to a file using io:fileWriteBlocksFromStream API by providing the file location to which the content should be written to.

            if (result is error) {
                log:printError("Error occurred while writing ",
                                'error = result);
            }
            close(payload);
        } else {
            log:printError("Error in parsing byte channel :",
                            'error = payload);
        }
    }
}
function getBaseType(string contentType) returns string {
    var result = mime:getMediaType(contentType);
    if (result is mime:MediaType) {
        return result.getBaseType();
    } else {
        panic result;
    }
}

Gets the base type from a given content type.

function close(stream<byte[], io:Error?> byteStream) {
    var cr = byteStream.close();
    if (cr is error) {
        log:printError("Error occurred while closing the stream: ",
                       'error = cr);
    }
}

Closes the byte stream.

# In the directory, which contains the `.bal` file, create a directory named `files`,
# and add an XML file named `test.xml` in it.
bal run response_with_multiparts.bal
[ballerina/http] started HTTP/WS listener 0.0.0.0:9090
[ballerina/http] started HTTP/WS listener 0.0.0.0:9092
time = 2021-01-21 22:20:38,143 level = INFO  module = "" message = "Nested Parts Detected!" 
time = 2021-01-21 22:20:38,185 level = INFO  module = "" message = "JSON data: {"name":"wso2"}" 
time = 2021-01-21 22:20:38,324 level = INFO  module = "" message = "XML data: <ballerinalang>
    <version>0.963</version>
    <test>test xml file to be used as a file part</test>
</ballerinalang>" 
#To encode the outbound response with multiparts.
curl -X GET http://localhost:9092/multiparts/encode_out_response
--5afd3d91ee639af3
content-type: multipart/mixed;boundary=de5520ef3bc703d7
--de5520ef3bc703d7
content-type: application/json
{"name":"wso2"}
--de5520ef3bc703d7
content-type: text/xml
<ballerinalang>
    <version>0.963</version>
    <test>test xml file to be used as a file part</test>
</ballerinalang>
--de5520ef3bc703d7--
--5afd3d91ee639af3--
#To decode the inbound response with multiparts.
curl -X GET http://localhost:9090/multiparts/decode_in_response
Body Parts Received!