Experiments‎ > ‎

Multipart HTTP file upload with JavaFX

For a pet project of mine some http file uploading is involved. It's easy to write a jsp page doing a multipart http post request with a selected file and fields. Doing it in JavaFX doesn't seem to work out of the box. That is, not as a multipart http request. And if it does, please be gentle when reminding me. :-)

I thought this might be useful to other people too. At the bottom of the page I'm including a link to the source code.

Consider the following jsp page :

<%@page contentType="text/html" pageEncoding="windows-1252"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=windows-1252"/>
        <title>Upload Document</title>
    </head>
    <body>
        <h1>Upload a document in Alexandria</h1>
        <form method='POST' enctype='multipart/form-data' action='wsrs/document'>
            <table border="0" width="60%">
                <tr>
                    <td>File</td>
                    <td><input type="file" name="upfile"/></td>
                </tr>
                <tr>
                    <td>File Name</td>
                    <td><input  type="text" name="fileName"/></td>
                </tr>
                <tr>
                    <td>Title</td>
                    <td><input  type="text" name="title"/></td>
                </tr>
                <tr>
                    <td>Notes</td>
                    <td><input  type="text" name="notes"/></td>
                </tr>
            </table>
            <input type="submit" value="Upload Document"/>
        </form>
    </body>
</html>

This page will allow the user to enter a title, notes and select a file from the local filesystem. Pressing the submit button uploads the file, together with the two fields to the original server. To achieve the same in JavaFX I've subclassed HttpRequest in a way the same operation is done as follows :

    def fileToUpload:File;

    ... code to select a local file ...

    def uploadRequest = FileUpload.HTTPRequest {
        location: http://localhost:8080/.....

        // sequence of files to upload

        files: [ fileToUpload ]

        // sequence of fields to upload
        fields: [ Pair {
                        name: "title"
                        value: "file title"
                     }
                     Pair {
                        name: "notes"
                        value: "file notes"
                     }
                   ]
    }

    uploadRequest.start();

Simply pass the a sequence of File objects (in case you want to upload more than one file) and a sequence of name/value Pair objects for the fields. Execute the start() function, just like you would do for a regular http get request.

The source code for the FileUpload.fx script :

import javafx.io.http.HttpRequest;
import java.io.File;
import java.io.OutputStream;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import javafx.io.http.HttpHeader;
import javafx.date.DateTime;
import javafx.data.Pair;

/**
 * HttpRequest able to upload files to a given url
 *
 * @author jan
 */

def PREFIX = "--";

def NEWLINE = "\r\n";

def BOUNDARY = createBoundary();

def CONTENT_TYPE = "multipart/form-data; boundary={BOUNDARY}";

def NAME = "upfile";

function createBoundary() :String {
    def now = DateTime {};
    return "{Long.toHexString(now.instant)}";
}

public class HTTPRequest extends HttpRequest {

    // A sequence of files to upload
    public-init var files:File[];

    // Fields to upload together with the request
    public-init var fields:Pair[];

    postinit{

        // we always do a post
        method = HttpRequest.POST;

        // override the onOutput
        onOutput = writeFiles;

        // add the headers we need
        insert HttpHeader {
            name:HttpHeader.ACCEPT
            value: "*/*"
        } into headers;
        insert HttpHeader {
            name:HttpHeader.CONTENT_TYPE
            value: CONTENT_TYPE
        } into headers;
        insert HttpHeader {
            name:HttpHeader.CONNECTION
            value: "keep-alive"
        } into headers;
        insert HttpHeader {
            name:HttpHeader.CACHE_CONTROL
            value: "no-cache"
        } into headers;
        insert HttpHeader {
            name:HttpHeader.ACCEPT_CHARSET
            value: "ISO-8859-1,utf-8;q=0.7,*;q=0.7"
        } into headers;
    }

    function writeFiles( output:OutputStream ) {
        try {
            // send the files
            if ( files != null and sizeof files > 0 ) {
                for ( file in files ) {
                    // leading characters
                    output.write(PREFIX.getBytes());
                    output.write(BOUNDARY.getBytes());
                    output.write(NEWLINE.getBytes());
                    output.write("Content-Disposition: form-data; name=\"{NAME}\"; filename=\"{file.getName()}\"".getBytes());
                    output.write(NEWLINE.getBytes());
                    output.write("Content-Type: application/octet-stream".getBytes());
                    output.write(NEWLINE.getBytes());
                    output.write(NEWLINE.getBytes());

                    // write the file's content on the wire - use a native array of byte
                    // in order to use Java's IO classes
                    var buffer = IOUtils.createByteBuffer(10240);
                    var input = new BufferedInputStream( new FileInputStream( file ) );
                    var eof = false;
                    try {
                        while ( true ) {
                            var read = input.read(buffer);
                            if ( read > 0 ) {
                                output.write(buffer, 0, read);
                            }
                            else {
                                break;
                            }
                        }
                    } finally {
                        input.close();
                    }

                    // end of file
                    output.write(NEWLINE.getBytes());
                    output.flush();
                }
                output.write(NEWLINE.getBytes());
            }

            // write the fields
            if ( fields != null and sizeof fields > 0 ) {
                for ( field:Pair in fields ) {
                    output.write(PREFIX.getBytes());
                    output.write(BOUNDARY.getBytes());
                    output.write(NEWLINE.getBytes());
                    // write content header
                    output.write("Content-Disposition: form-data; name=\"{field.name}\"".getBytes());
                    output.write(NEWLINE.getBytes());
                    output.write(NEWLINE.getBytes());
                    // write content
                    output.write("{field.value}".getBytes());
                    output.write(NEWLINE.getBytes());
                    output.flush();
                }
            }
            output.write(PREFIX.getBytes());
            output.write(BOUNDARY.getBytes());
            output.write(PREFIX.getBytes());
            output.write(NEWLINE.getBytes());

        } finally {
            output.flush();
            output.close();
        }
    }
}

I'm not that experienced with low-level HTTP, but it seems to work just fine. For those who wonder why I created the IOUtils Java class, this is to create a native byte array for doing the I/O. The Java I/O classes won't accept Byte sequences. Neither can they be cast to native byte arrays. I haven't found the way yet to do this in JavaFX. As such this class is rather straightfoward :

public class IOUtils {
  public static final byte[] createByteBuffer( int length ) {
        return new byte[length];
    }
}

There is no doubt this code can be optimized or corrected in some way. For my purposes it does the job. For the sake of being complete I'm including a basic upload dialog with which the user can select a file and upload it to the a hardcoded url.

import java.io.File;
import javax.swing.JFileChooser;

import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.layout.VBox;
import javafx.scene.control.Label;
import javafx.scene.control.TextBox;
import javafx.scene.layout.LayoutInfo;
import javafx.scene.control.Button;
import javafx.data.Pair;

import FileUpload.*;
import java.lang.Exception;

/**
 * @author jgoyvaer
 */

def WIDTH = 400;
def HEIGHT = 160;
def UPLOAD_URL = "http://localhost:8080/.....";

var fileToUpload:File;
var fileNameToUpload:String;
var progressUpload:Float;
var bytesToUpload:Float;

var uploadRequest:HttpRequest;

function start() {
    progressUpload = 0;
    bytesToUpload = 0;
    uploadRequest = HttpRequest {
        location: UPLOAD_URL
        files: [ fileToUpload ]
        fields: [
                  Pair {
                    name: "title"
                    value: "file title"
                  }
                  Pair {
                    name: "notes"
                    value: "file notes"
                  }
                ]
        onToWrite: function( size:Long ) {
            bytesToUpload = size;
        }
        onWritten: function( size:Long ) {
            progressUpload = 1.0 * size / bytesToUpload;
        }
        onDone: stop
        onException: function( ex:Exception ) {
            println(ex.getMessage());
            stop();
        }
    }
    uploadRequest.start();
}

function stop() {
    progressUpload = 0;
    bytesToUpload = 0;
    if ( uploadRequest != null ) {
        uploadRequest.stop();
        uploadRequest = null;
    }
}


Stage {
    title: "Uploading files over HTTP"
    scene: Scene {
        width: WIDTH
        height: HEIGHT
        content: HBox {
            spacing: 5
            content: [
                        ProgressIndicator {
                            progress: bind progressUpload
                            layoutInfo: LayoutInfo {
                                width: 100
                                height: 150
                            }
                            scaleX: 4
                            scaleY: 4
                        }
                        VBox {
                            spacing: 5
                            content: [
                                        Label {
                                            text:"File to upload"
                                        }
                                        HBox {
                                            spacing: 2
                                            content: [
                                                    TextBox {
                                                        text: bind fileNameToUpload with inverse
                                                        layoutInfo: LayoutInfo {
                                                            width: WIDTH - 150;
                                                        }
                                                    }
                                                    Button {
                                                        text:"..."
                                                        action: function() {
                                                            def fc = JFileChooser{};
                                                            if ( fc.showOpenDialog(null) == JFileChooser.APPROVE_OPTION ) {
                                                                fileToUpload = fc.getSelectedFile();
                                                                fileNameToUpload = fileToUpload.getAbsolutePath();
                                                            }
                                                        }
                                                    }
                                                ]
                                        }
                                        Button {
                                            text: bind if ( uploadRequest != null ) "Cancel" else "Upload";
                                            disable: bind fileToUpload == null;
                                            action: function() {
                                                if ( uploadRequest != null ) {
                                                    stop();
                                                }
                                                else {
                                                    start();
                                                }
                                            }
                                        }
                                     ]
                        }
                     ]
        }
    }
}

The dialog script is using Swing's JFileChooser. As a result I guess this code will only run in a desktop profile application. As a final note I would advise to use JFXtras' MigLayout to shorten the coding a bit. It would add an additional dependency with the desktop profile though.

Please let me know if this code was any useful to you. :-)
 
FileUpload.zip
Download
Netbeans 6.5.1 project with the source code of this article.  9k v. 1 Jul 9, 2009 4:05 PM Jan Goyvaerts