Wicket – Ajax like file upload on a modal window

35 Comments »

A lot of people seem to be asking how to do a file upload using AJAX. The short answer to this is that it cannot be done! That’s because it is not supported by today’s HTML / browser specs.

What you can do is just fake it :-) .

I’ve been using Wicket (Java Web Framework) regularly for web development. Recently I needed to be able to do a file upload inside a modal window. File uploading needs form submission in order to work so I faced a few problems:

  • My modal window was implemented as a panel. I could not do a regular form submission as that would close (submit) my modal window. I’ve been suggested to implement the modal window as a page instead of the panel so that I could use Ajax submit but I really needed it the way it was.
  • Ajax and multipart don’t cope.
  • Whatever I tried, without regular submission, the file uploading field would not return the file name so it was impossible to get.

I searched the web and I finally found and implemented the solution. It involves using an iframe, but it’s pretty simple. Here’s the code if anyone else needs this:

  • UploadIFrame.java
public abstract class UploadIFrame extends WebPage {

    private boolean uploaded = false;
    private FileUploadField uploadField;
    private String newFileUrl;

    public UploadIFrame() {
        add(new UploadForm("form"));
        addOnUploadedCallback();
    }

    /**
     * return the callback url when upload is finished
     * @return callback url when upload is finished
     */
    protected abstract String getOnUploadedCallback();

    /**
     * Called when the input stream has been uploaded and when it is available
     * on server side
     * return the url of the uploaded file
     * @param upload fileUpload
     */
    protected abstract String manageInputSream(FileUpload upload);

    private class UploadForm extends Form {
    	public UploadForm(String id) {
            super(id);
            uploadField = new FileUploadField("file");
            add(uploadField);
            add(new AjaxLink("submit"){
            	@Override
            	public void onClick(AjaxRequestTarget target) {
            		target.appendJavascript("showProgressWheel()");
            	}
            });
        }

    	@Override
        public void onSubmit() {
            FileUpload upload = uploadField.getFileUpload();
            newFileUrl = manageInputSream(upload);
            //file is now uploaded, and the IFrame will be reloaded, during
            //reload we need to run the callback
            uploaded = true;
        }

    }

    private void addOnUploadedCallback() {
    	//a hacked component to run the callback on the parent
        add(new WebComponent("onUploaded") {
        	@Override
            protected void onComponentTagBody(MarkupStream markupStream, ComponentTag openTag) {
                if (uploaded) {
                	if (uploadField.getFileUpload() != null){
	                    replaceComponentTagBody(markupStream, openTag,
	                            "window.parent." + getOnUploadedCallback() + "('" +
	                            uploadField.getFileUpload().getClientFileName() + "','" +
	                            newFileUrl +"')");
                	}
                    uploaded = false;
                }
            }
        });
    }
}
  • UploadIFrame.html


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns:wicket>
<head>
<link href = "../counters/css/style.css" type = "text/css" rel = "stylesheet"/>
<!--[if IE]>
<link href = "../counters/css/ie.css" type = "text/css" rel = "stylesheet"/>
<![endif]-->
<script type="text/javascript">
	function showProgressWheel() {
		document.images[0].style.display = 'block';
	  	// delay the wheel a bit so it can locally shine
	  	setTimeout(function() { document.forms[0].submit() }, 800);
	  	return false;
	}
</script>
</head>

<body>
	<form wicket:id="form">
		<table style="width:500px; margin-top:0px;">
	  		<tr>
	  			<td>
	    			<input style="width:auto;" wicket:id="file" type="file"/>
	    		</td>
	    		<td>
					<a wicket:id="submit">Change Avatar</a>
				</td>
				<td>
	    			<img src="images/indicator.gif" style="display:none"/>
	    		</td>
	    	</tr>
	    </table>
  </form>

  <script wicket:id="onUploaded" type="text/javascript"></script>
</body>
</html>
  • UploadPanel.java
public abstract class UploadPanel extends Panel {

    private InlineFrame uploadIFrame = null;

    public UploadPanel(String id) {
        super(id);
        addOnUploadedCallback();
        setOutputMarkupId(true);
    }

    /**
     * Called when the upload load is uploaded and ready to be used
     * Return the url of the new uploaded resource
     * @param upload {@link FileUpload}
     */
    public abstract String onFileUploaded(FileUpload upload);

    /**
     * Called once the upload is finished and the traitment of the
     * {@link FileUpload} has been done in {@link UploadPanel#onFileUploaded}
     * @param target an {@link AjaxRequestTarget}
     * @param fileName name of the file on the client side
     * @param newFileUrl Url of the uploaded file
     */
    public abstract void onUploadFinished(AjaxRequestTarget target, String filename, String newFileUrl);

    @Override
    protected void onBeforeRender() {
        super.onBeforeRender();
        if (uploadIFrame == null) {
            // the iframe should be attached to a page to be able to get its pagemap,
            // that's why i'm adding it in onBeforRender
            addUploadIFrame();
        }
    }

    /**
     * Create the iframe containing the upload widget
     *
     */
    private void addUploadIFrame() {
        IPageLink iFrameLink = new IPageLink() {
        	@Override
            public Page getPage() {
                return new UploadIFrame() {
                	@Override
                    protected String getOnUploadedCallback() {
                        return "onUpload_" + UploadPanel.this.getMarkupId();
                    }

                    @Override
                    protected String manageInputSream(FileUpload upload) {
                    	return UploadPanel.this.onFileUploaded(upload);
                    }
                };
            }
        	@Override
            public Class<UploadIFrame> getPageIdentity() {
                return UploadIFrame.class;
            }
        };
        uploadIFrame = new InlineFrame("upload", getPage().getPageMap(), iFrameLink);
        add(uploadIFrame);
    }

    /**
     * Hackie method allowing to add a javascript in the page defining the
     * callback called by the innerIframe
     *
     */
    private void addOnUploadedCallback() {
        final OnUploadedBehavior onUploadBehavior = new OnUploadedBehavior();
        add(onUploadBehavior);
        add(new WebComponent("onUploaded") {
        	@Override
            protected void onComponentTagBody(MarkupStream markupStream, ComponentTag openTag) {
                // calling it through setTimeout we ensure that the callback is called
                // in the proper execution context, that is the parent frame
                replaceComponentTagBody(markupStream, openTag,
                        "function onUpload_" + UploadPanel.this.getMarkupId() +
                        "(clientFileName, newFileUrl) {window.setTimeout(function() { " +
                        onUploadBehavior.getCallback() + " }, 0 )}");
            }
        });
    }

    private class OnUploadedBehavior extends AbstractDefaultAjaxBehavior {
        public String getCallback() {
            return generateCallbackScript(
                    "wicketAjaxGet('" + getCallbackUrl(false) +
                    "&amp;amp;newFileUrl=' + encodeURIComponent(newFileUrl)" +
                    " + '&amp;amp;clientFileName=' + encodeURIComponent(clientFileName)").toString();
        }
        @Override
        protected void respond(AjaxRequestTarget target) {
        	UploadPanel.this.onUploadFinished(target, getRequest().getParameter("clientFileName"), getRequest().getParameter("newFileUrl"));
        }
    };
}
  • UploadPanel.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns:wicket>
<wicket:panel>
  <!-- I put the callback snippet at the body so that is rendered for each panel instead of once -->
  <script wicket:id="onUploaded" type="text/javascript"></script>
  <iframe wicket:id="upload" frameborder="0" style="height:60px; width:500px; overflow:hidden"></iframe>
</wicket:panel>
</html>
  • This is how to use it:
		add(new UploadPanel("myUpload"){
			@Override
			public String onFileUploaded(FileUpload upload) {
				if (upload != null){
					if(upload.getSize() > 1000000) {
						return "The Picture Is Too Big !";
					} else {
				        try {
							jdbcSignUp.changeAvatarFor(((WebSession)getSession()).getUser().getUsername(), upload.getInputStream(), (int)upload.getSize());
				        	//save on server side here
				        	//and return the url of the saved file
	                        return "The Picture Has Been Uploaded";
				        } catch (IOException e) {
							log.error("Exception:", e);
						}
					}
				}
				return "";
			}

			@Override
			public void onUploadFinished(AjaxRequestTarget target, String filename, String newFileUrl) {
				//when upload is finished, will be called
				messageLabel.setModelObject(newFileUrl);
				target.addComponent(messageLabel);

				avatarImage.setImageResource(new MyBlobImageResource(((WebSession)getSession()).getUser().getUsername()));
				target.addComponent(avatarImage);
				((BasePage)parent).onAvatarChanged(target);
			}
		});

Most credit should go to Vincent Demay, although his code contains errors.