• Blog
  • Integrating Uploadify with Java using Apache Wicket

Integrating Uploadify with Java using Apache Wicket

April 30, 2010

One of the most requested features on any site that accepts file submissions is the ability to handle multiple uploads elegantly. The current HTML standard only permits one file upload at a time, which makes transferring a large number of files quite tedious.

This tutorial will show how to painlessly integrate Uploadify and Java using Apache Wicket. You should have at least beginner-level knowledge of Wicket before moving forward, but feel free to dive right in even if you’ve never used Wicket before. (If you haven’t used Wicket before, I can’t suggest it strongly enough for developing stateful Java-based web applications.)

A number of excellent Flash-based multiple file upload plugins are available, including the excellent Uploadify. Uploadify is essentially a jQuery wrapper for SWFUpload. I already use jQuery quite extensively so I chose Uploadify as my multiple upload plugin. If you use a different JavaScript framework or simply prefer plain old JavaScript, SWFUpload might be a better choice.

The only catch is that most of these plugins are geared towards PHP developers/apps and not all that intuitive to integrate with Java-based solutions.

A few things to consider

Before jumping in with both feet, the use of Flash is a stopgap measure until HTML5 is more widely supported. HTML5 supports the attribute “multiple” in the input tag, offering true native multiple file upload support. Also, with Apple fully throwing its weight behind HTML5 and actively attacking Flash, it’s only a matter of time before Flash-based solutions go the way of the bond market in Greece.

While HTML5 supports the attribute “multiple” in the input tag, browser support for HTML5 is far from ubiquitous. To make matters worse, the HTML5 standard won’t officially be signed off on until 2022. Yes, 12 more years. Yes, it’s completely ridiculous. That’s how design-by-committee works.

Getting Started

Preamble complete, let’s get down to business. Our strategy will be to create a re-usable Apache Wicket “multiple file upload” component. In our case, because we will be re-using both Java code and HTML, we’ll create a Wicket panel to do this.

1. Dependencies

We don’t need everything included in the Uploadify download. We’ll create new packages for the components, CSS, and scripts, and cherry-pick the Uploadify artifacts to copy to the new project. Depending on how you have Wicket configured, your scripts, CSS, and HTML files may be separate from Java source files. The default in Wicket is to place them together in the same package, so we’ll be using that paradigm for this tutorial.

2. Re-usable HTML

The next step is to create the HTML for our Panel. Create a new file called UploadifyPanel.html with the following source:

<wicket:panel>

<div id="fileQueue">
	<p><strong>Click browse to select files for uploading.</strong></p>
	<p><em>To select more than one file, hold down control or shift.</em></p>
	<p><input type="file" name="uploadify" id="uploadify" /></p>
</div>

</wicket:panel>

Most of what you see above is standard HTML and CSS. The only item of note is the input tag, which will be replaced with the Flash component by the Uploadify script. If the user does not have Flash installed, the above input tag will be used as-is without replacement, so make sure the tag is valid for graceful downgrading.

If you want to enhance the tutorial, you should use Wicket’s excellent localization support to add fully configurable dynamic text to the re-usable component.

3. Java components for re-usable Panel

package org.rocketpages.wicket.component.uploadify;

import org.apache.wicket.markup.html.CSSPackageResource;  
import org.apache.wicket.markup.html.JavascriptPackageResource;  
import org.apache.wicket.markup.html.panel.Panel;  
import org.apache.wicket.util.template.TextTemplateHeaderContributor;

public class UploadifyPanel extends Panel {
	private static final long serialVersionUID = 1L;
	
	public UploadifyPanel(String id, UploadifyPanelBehaviour behaviour) {
	    super(id);
	
	    add(JavascriptPackageResource.getHeaderContribution(UploadifyPanel.class, "scripts/swfobject.js"));   
	    add(JavascriptPackageResource.getHeaderContribution(UploadifyPanel.class, "scripts/jquery.uploadify.v2.1.0.js"));
	
	    /* TextTemplateHeaderContributor performs text substitution of values located
	     in multiupload.js with the values obtained from the behaviour, and
	     inserts the processed .js in the header. */
	    add(TextTemplateHeaderContributor.forJavaScript(UploadifyPanel.class, "multiupload.js", behaviour.getVariables()));
	    add(CSSPackageResource.getHeaderContribution(UploadifyPanel.class, "css/uploadify.css"));
	}   
}

For those of you familiar with Wicket, most of this will look straightforward enough. We’re encapsulating all of our configuration variables in a behaviour, which keeps our code clean and generic. Of special interest is the call to TextTemplateHeaderContributor.forJavaScript(). This is a handy little method that allows you to pass in a Map of variables (key/value) and a reference to a JavaScript file. This results in token substitution in the JavaScript file based on the values in the Map. It’s much more simple than performing manual string concatenation to achieve the same result.

public class UploadifyPanelBehaviour implements Serializable {
	private String fileExt = "";
	private String fileDesc = "";
	private String sizeLimit = "";

	private final String formTokenId;
	private final String formSessionId;

	private Class<? extends UploadifyFileProcessPage> fileProcessPageClass;

	public UploadifyPanelBehaviour(Class<? extends UploadifyFileProcessPage> fileProcessPageClass, String tokenId, String sessionId)
	{
		this.fileProcessPageClass = fileProcessPageClass;
		this.formSessionId = sessionId;
		this.formTokenId = tokenId;
	}

	/**
	 * Sets the valid file extensions for the uploader.
	 * 
	 * Do not include leading or trailing quotes, only semi-colon
	 * separated wildcards. 
	 * 
	 * Example usage:
	 * *.doc;*.docx;*.pdf;*.jpg;*.png;*.zip
	 * 
	 * @param validExts
	 */
	public void setFileExt(String validExts)
	{
		this.fileExt = validExts;
	}
	
	/**
	 * Sets the message to be displayed when selecting files.
	 * 
	 * Example message:
	 * Select files of type .doc, .pdf, .jpg, .png, or .zip
	 * 
	 * @param fileDesc
	 */
	public void setFileDesc(String fileDesc)
	{
		this.fileDesc = fileDesc;
	}
	
	/**
	 * Sets the size limit, in bytes.
	 * 
	 * Example: 3072000
	 * 
	 * @param sizeLimit
	 */
	public void setSizeLimit(String sizeLimit)
	{
		this.sizeLimit = sizeLimit;
	}
	
	/**
	 * Builds a variables model in order to perform text substitution
	 * on values located in JavaScript (multiupload.js)
	 * 
	 * The key values in the Map must match the tokens in the .js file. 
	 * 
	 * @return
	 */
	protected IModel getVariables() {
		IModel variablesModel = new AbstractReadOnlyModel() {
			public Map getObject() {
				Map<String, CharSequence> variables = new HashMap<String, CharSequence>();

				//Mandatory configuration
				variables.put("uploader", RequestCycle.get().urlFor(new CompressedResourceReference(UploadifyPanel.class, "scripts/uploadify.swf")).toString());
				variables.put("cancelImg", RequestCycle.get().urlFor(new CompressedResourceReference(UploadifyPanel.class, "cancel.png")).toString());
				variables.put("script", ";jsessionid=" + formSessionId + RequestCycle.get().urlFor(fileProcessPageClass, null));
				variables.put("tokenId", formTokenId);

				//Optional configuration
				variables.put("fileExt", fileExt);
				variables.put("fileDesc", fileDesc);
				variables.put("sizeLimit", sizeLimit);
	
				return variables;
			}
		};
	
		return variablesModel;
	}
}

The behaviour captures configuration options, which correspond to the available options within Uploadify. You can add more configuration options (or remove some) depending on your specific requirements.

Another quirk to pay attention to is the token and session id values. This is a kludge we’ve added to avoid the messy problem of Flash losing a reference to the HttpSession. Because the end result of this script will be a call to a stateless page to process our uploads, we’ll need some way to retrieve the user’s session. Because of this, we need to include the session ID as a value in the JavaScript file and pass this as a parameter to our stateless file processing page.

Finally, make special note of the class we’re passing to our behaviour object. We do this because Wicket needs to “mount” our stateless file processing page so it can be accessed by the plugin. Wicket instantiates the page “just in time”, not before, otherwise it would hang around in memory doing nothing (kinda like a TTC collector). Note the call to RequestCycle.get().urlFor().

public abstract class UploadifyFileProcessPage extends WebPage {

	protected User user;
	
	public UploadifyFileProcessPage(PageParameters params) {
	    HttpServletRequest r = ((WebRequest) RequestCycle.get().getRequest()).getHttpServletRequest();
	    try
	    {
	        MultipartServletWebRequest r2 = new MultipartServletWebRequest(r, Bytes.MAX);
	        Map paramMap = r2.getParameterMap();
	        String[] tokenIdParms = (String[]) paramMap.get("tokenId");
	        String tokenId = tokenIdParms[0];
	        UserSession session = (UserSession) Session.get();
	        FormToken token = session.getFormToken();
	
	        if (token.isValid(tokenId, r.getRemoteAddr()))
	        {
	            long userId = session.getUser().getId();
	            user = new UserServiceImpl().getUser(userId);
	
	            for (FileItem fi : r2.getFiles().values())
	            {
	                processFileItem(fi);
	                fi.delete();
	            }
	        }
	        else
	        {
	            throw new Exception("Invalid token!");
	        }
	    }
	    catch (Exception e)
	    {
	        e.printStackTrace();
	    }
	}
	
	protected abstract void processFileItem(FileItem fileItem) throws Exception;
}

In order to bridge our plugin with Java, we’re creating a stateless page by extending WebPage. This base code is fairly straightforward, pulling out the file data from the request, and passing it to the yet-to-be-defined abstract method processFileItem().

The only other thing to note is the token logic. This is simply an example/placeholder to draw your attention to security when dealing with jsessionid directly. This leaves a small window open for hackers to perform a session hijacking, so in our scenario, we associate a jsessionid with an IP address and an additional token value.

$(document).ready(function() { 
	$('#uploadify').uploadify({ 
	    'uploader':  '${uploader}', 
	    'script':   '${script}',
	    'scriptData': {'tokenId': '${tokenId}'}, 
	    'folder': '/var/photos/stuffahoy', 
	    'cancelImg': '${cancelImg}',
	    'fileExt' : '${fileExt}',
	    'fileDesc' : '${fileDesc}',
	    'sizeLimit' : '${sizeLimit}',
	    'auto': true,
	    'multi': true,
	    onAllComplete: function(event, queueID, fileObj, response, data) {
	        uploadCompleted(); 
	    }
	}); 
});

If you’re familiar with JavaScript, you’ll see we’re essentially passing an object to the uploadify() method. The key values of the object correspond to the Uploadify options. The token values on the right correlate to our behaviour class and the key values we defined. Text substitution is done on these when we include the JavaScript file via the call to TextTemplateHeaderContributor.

4. Business Logic

public class AddPhotosProcessingPage extends UploadifyFileProcessPage {
	public AddPhotosProcessingPage(PageParameters params)
	{
	    super(params);
	}
	
	@Override
	protected void processFileItem(FileItem fileItem) throws Exception
	{
	    //Wrap the Apache Commons FileItem type in a Wicket FileUpload type
	    FileUpload upload = new FileUpload(fileItem);
	    //Get a service reference, store the photo, and create a new Photo record
	    PhotoService photoService = new PhotoServiceImpl();
	    Photo photo = photoService.saveUploadedPhoto(upload, true);
	    photo.setUser(user);
	    photoService.savePhoto(photo);
	}
}

This is our actual stateless page that extends our more generic file processor. The code is an example of business logic you may execute on each file. In this case, we’re processing image files, so we pass the file reference to a service which in turn writes the file to disk (or Amazon S3, etc) and persists the information in our database.

public class AddPhotosPage extends UserRoleSuperPage {
	private static final long serialVersionUID = 1L;
	
	private Label noPhotosLabel;
	
	public AddPhotosPage(final PageParameters parameters) {
	    this();
	}
	
	public AddPhotosPage() {
	    super(null);
	
	    add(CSSPackageResource.getHeaderContribution(AddPhotosPage.class, 
	        "css/AddPhotosPage.css"));
	
	    UserSession session = (UserSession) Session.get();
	    long userId = session.getUser().getId();
	
	    HttpServletRequest r = ((WebRequest) RequestCycle.get().getRequest()).getHttpServletRequest();
	    session.createFormToken(r.getRemoteAddr());
	
	    String tokenId = session.getFormToken().getToken();
	    String sessionId = session.getId();
	
	    // Configure the panel
	    UploadifyPanelBehaviour behaviour = new UploadifyPanelBehaviour(AddPhotosProcessingPage.class, tokenId, sessionId);
	    behaviour.setFileExt("*.jpg;*.jpeg;*.gif;*.png");
	    behaviour.setFileDesc("Select files of type *.jpg, *.jpeg, *.gif, *.png");
	    behaviour.setSizeLimit("3000000");
	
	    add(new UploadifyPanel("uploads", behaviour));
	
	    final UnprocessedPhotosPanel unprocessedPanel;
	    add(unprocessedPanel = new UnprocessedPhotosPanel("unprocessedPhotos", userId));
	    unprocessedPanel.setOutputMarkupId(true);
	
	    final Label noPhotosLabel;
	    add(noPhotosLabel = new Label("noPhotosLabel", "You haven't uploaded any new photos!"));
	    noPhotosLabel.setOutputMarkupId(true);
	
	    if (unprocessedPanel.getUnprocessedPhotosCount() > 0) {
	        noPhotosLabel.setVisible(false);
	    }
	    else {
	        noPhotosLabel.setVisible(true);
	    }
	
	    final IBehavior repaintBehavior = new AbstractDefaultAjaxBehavior() {
	        @Override
	        protected void respond(AjaxRequestTarget target)
	        {
	            if (unprocessedPanel.getUnprocessedPhotosCount() > 0) {
	                noPhotosLabel.setVisible(false);
	            }
	            else {
	                noPhotosLabel.setVisible(true);
	            }
	
	            target.addComponent(noPhotosLabel);
	            target.addComponent(unprocessedPanel);
	        }
	
	        @Override
	        public void renderHead(IHeaderResponse response)
	        {
	            super.renderHead(response);
	            CharSequence callback = getCallbackScript();
	            response.renderJavascript("function uploadCompleted() { " + callback + "}", "customUploadCompleted");
	        }
	    };
	
	    add(repaintBehavior);
	
	    add(new Image("easy", new ResourceReference(AddPhotosPage.class, "easy.png"))); 
	}
}

This example is a little long-winded, but it ties everything together and shows a few key concepts. Most interesting is our ability to asynchronously invoke a piece of code when all uploads are complete by using Wicket’s fantastic built in Ajax support. We do this by creating a new repaintBehaviour object and overriding both respond (which contains the actual code we want to invoke asynchronously) and renderHead (which injects this callback method inside our HTML).

Other than that, most things are quite straightforward. We tie everything together by configuring our behaviour object and instantiating the processing page.

Conclusion

Enabling true multiple upload support is fairly straightforward with the Flash plugins readily available. Uploadify is my favourite, but there are a ton to choose from. Integrating with Apache Wicket is a piece of cake, and as always, Wicket just makes life a little easier for Java developers.