Dojo Ajaxified multi-file upload (including IE7)

One of the most common requirements of business applications is to develop Ajaxified multi-file upload. Thanks to Dojo 1.6 (and later), you can achieve this requirement, however, there are some tricks that you need to do in order to have this feature developed across all the browsers including IE. Let’s start to see the code.

The following listing shows the HTML code part of the file uploader.

<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<!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=ISO-8859-1">
	<title>File Upload POC</title>
	
	<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/dojo/1.7.2/dojo/dojo.js">
	</script>
</head>
<body>
	<script type="text/javascript" src="${pageContext.request.contextPath}/js/fileUploader.js"></script>
	<div id="container">
		<form method="post" id="myForm" enctype="multipart/form-data">
		    <fieldset>
		        <legend>Form Files Test</legend>
		        
		        <input id="clearBtn" type="button" value="Clear"></input><br/><br/>
				<div id="uploader"></div><br/>
				<div id="uploaderStatus"></div>
      					
		        <input id="uploadBtn" type="button" value="Submit"></input>
		    </fieldset>
		</form>
	</div>
</body>
</html>

As shown the file upload HTML, the form’s enctype needs to be set to “multipart/form-data”. The form contains “uploader” div that represents the Ajaxified file upload component and “uploaderStatus” div that represents the status panel of the selected files and finally it contains the upload button that will be used for uploading.

Now, Let’s look at the JavaScript file (fileUploader.js) which uses the dojos/form/Uploader module.

require([
        "dojo/parser", "dojox/form/Uploader", "dojo/dom", "dojo/on", "dojo/has", 
        "dojox/form/uploader/FileList", "dojox/form/uploader/plugins/IFrame", 
        "dojo/domReady!"], 
function(parser, Uploader, dom, on, has) {    
    parser.parse(document.getElementById("container"));
    
    var uploaderDIV = document.getElementById("uploader");
    
    var up = new dojox.form.Uploader({
        label: 'Select files',
        style : 'background-color: #ddddff; border: solid 1px;', //Externalize ...
        multiple: true,
        url: "/multifile-ajax-poc/UploaderServlet"
    }).placeAt(uploaderDIV);
    
    on (dom.byId("uploadBtn"), "click", function(evt) {
    	
    	//You can put some validations here ...
        up.submit();
    });  

    on (dom.byId("clearBtn"), "click", function(evt) {
        dom.byId("uploaderStatus").innerHTML = "";
        up.reset();
    });
    
    dojo.connect(up, "onComplete", function(dataArray) {
        var i = 0;
        
        dom.byId("uploaderStatus").innerHTML = "";
        
        if (!dataArray.error) {
            for (i = 0; i < dataArray.length; ++i) {
            	dom.byId("uploaderStatus").innerHTML += "File ID is: " + dataArray[i].id + " is uploaded" + "<br/>";
            }
        } else {
        	dom.byId("uploaderStatus").innerHTML = "Unable to upload the file(s)";
        }
    });    
    
    dojo.connect(up, "onChange", function(evt) {
        var i = 0;
        var content = "";
        var dataArray = up.getFileList();
        
        for (i = 0; i < dataArray.length; ++i) {
            content += dataArray[i].name + "<br/>";
        }
        
        dom.byId("uploaderStatus").innerHTML = content;
    });    
    
    up.startup();     
}
);

As shown in the code, the Dojo Ajaxified file uploader is created using dojox.form.Uploader constructor with specifying the main important attributes:
1. label.
2. multiple attribute is set to true in order to support multi-file uploading.
3. url attribute is set to the Java Servlet which handles the file uploading.

In order to get the uploader working fine, you need to call startup() method of the component.

There are important methods that are needed to be considered:
1. submit() method which allows submitting the selected files to the Java Servlet specified in the url attribute.
2. reset() method which is used to clear the selected files. Note that one of the limitations of this component is that it does not allow removing the selected files individually so the only option you have is ti clear all the files and select them again.
3. getFileList() method which is used to get the list of selected files that you can display for example in the “on change” event of the component as shown by the example.

One important thing to note is dojox/form/uploader/plugins/IFrame plugin which is essential for non-HTML5 browser in order to perform Ajaxified file uploads. Using this plugin will allow file uploading to be done through iframe for poor browsers like IE7.

Finally, let’s come to the most important trick of Ajaxified file uploading which is the server side part. The following code listing shows UploaderServlet Java Servlet.

package servlets;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Iterator;
import java.util.List;
import java.util.Random;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

@WebServlet("/UploaderServlet")
public class UploaderServlet extends HttpServlet {
	private static final long serialVersionUID = 1809724554045451657L;

	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		DiskFileItemFactory factory = new DiskFileItemFactory();
		ServletFileUpload upload = new ServletFileUpload(factory);
		String result = "";
		
		try {
			List<FileItem> items = upload.parseRequest(request);
			Iterator<FileItem> iter = items.iterator();
			int index = 0;

			result += "[";
			
			while (iter.hasNext()) {
			    FileItem item = iter.next();

			    if (!item.isFormField()) {
				    if (index != 0) {
				    	result += ", ";
				    }
				    
			        String feedback = processUploadedFile(item);
			        
			        //Handle IE7 ugly uploading bug
			        if (feedback == null) {
			        	continue;
			        } else {
			        	result += feedback;
			        }
			        
				    ++index;
				    
				    System.out.println(index);
			    }
			}
			
			result += "]";
			
			System.out.println(result);
			
		} catch (FileUploadException e) {
			result = "{'error':'" + e.getLocalizedMessage() + "'}";
			e.printStackTrace();
		}
		
		respondToClient(request, response, result);
	}

	private String processUploadedFile(FileItem item) {
		byte[] data = item.get();
	    String fileName = item.getName();
	    String contentType = item.getContentType();
	    
	    // Handle IE7 file uploading ugly bug ...
	    if (fileName.equals("")) {
	    	return null; //ignore
	    }
	    
		System.out.println(fileName + ", " + contentType + ", " + data.length);	    
	    
	    return "{'fileName':'" + fileName + "', " + 
	    		"'contentType':'" + contentType + "', " + 
	    		"'size':" + data.length + ", " + 
	    		"'id':" + (System.currentTimeMillis() + new Random().nextInt(100)) + "}";
	}
	
	private void respondToClient(HttpServletRequest request, HttpServletResponse response, String result) throws IOException {
        response.setContentType("text/html");
		PrintWriter writer = response.getWriter();		
		String browser = request.getHeader("User-Agent");
		
		if (browser.indexOf("MSIE 9") &gt; 0 || browser.indexOf("MSIE 8") &gt; 0 || browser.indexOf("MSIE 7") &gt; 0 ) {
			
			// For IE 9, 8, and 7 browser, render JSON object inside text area ...
			//String sampleOutput = "<textarea>[{'success':'true', 'id':'123456789'}]</textarea>";
			writer.write("<textarea>" + result + "</textarea>");
		} else {
			
			//For non-IE browsers, render normal JSON objects.
			//String sampleOutput = "[{\"success\":\"true\", \"id\":\"123456789\"}]";
			writer.write(result.replace("'", "\""));
		}		

		writer.flush();		
	}
}

Our UploaderServlet main purpose is to receive the files and if it succeeds to process the files, then it returns a simple array with the basic information of the files that are successfully uploaded.

The most important thing to notice here is for IE7 and IE8 and IE9 (NOT IE 10), a special handling is needed to be taken into consideration in the Servlet response:
1. The response data must be wrapped into textarea field.
2. Do not use double quotes (“) inside the returned JSON array, instead use single quote (‘).

In other modern browsers (which support HTML5 file uploading), you do not need to wrap the content in textarea as it does not use iframe for uploading, and you can freely use the double quotes (“) in the JSON array.

There is an ugly bug that you need to take care about in IE 7 and 8 and also 9 with the Dojo file uploader component, this bug is about having a redundant file with non-name that is always sent in the request of the multiple-file Ajax uploading. In order to handle this bug, just ignore the file which does not have a name (Ugly problem and ugly fix :-)).

This is all about my experience in this component, I wish that it can be helpful to you.

Download the code sample

This entry was posted in Dojo, JavaScript, Web 2.0 and tagged , , , , , , , , , , , , , by Hazem Saleh. Bookmark the permalink.

About Hazem Saleh

Hazem Saleh has more than eleven years of experience in Cloud, Mobile and Open Source technologies. He worked as a software engineer, technical leader, application architect, and technical consultant for many clients around the world. He is an Apache PMC (Project Management Committee) member and a person who spent many years of his life writing open source software. Beside being the author of the "JavaScript Unit Testing" book, "JavaScript Mobile Application Development" book, "Pro JSF and HTML5" book and the co-author of the "Definitive guide to Apache MyFaces" book, Hazem is also an author of many technical articles, a developerWorks contributing author and a technical speaker in both local and international conferences such as ApacheCon North America, Geecon, JavaLand, JSFDays, CON-FESS Vienna and JavaOne. Hazem is an XIBMer, he worked in IBM for ten years. Now, He is working for Nickelodeon New York as a Mobile Architect. He is also an OpenGroup Master Certified Specialist.