AJAX-like File Upload and Download in Grails

written by phil on May 21st, 2008 @ 09:14 AM

I recently had the opportunity to create a simple application in Grails. The application was nothing more than an ETL process, with a single screen for uploading and downloading files. I wanted to be able to provide the user feedback without going through a redirect-after-post scenario. It turns out that this is relatively straightforward, once you know all the details.

First, I say “AJAX-like” file upload because it’s not actually possible to send multipart encoded form data through an XMLHttpRequest object (AJAX’s core communications object). However, we can achieve similar functionality by using an IFRAME. Here’s an example of what the form might look like in Grails:

<div id="status" style="height:100px;width: 510px;margin: 10px auto;padding: 10px;">
  <div class="message" id="message" style="display:${flash.message ? 'block':'none'}">
${flash.message}</div>
  <div class="errors" id="errors" style="display:${flash.error ? 'block':'none'}">
${flash.error}</div>
  <div id="upload_progress" style="display:none;">Loading...</div>        
</div>
<div id="content">
  <g:form method="post" action="save" enctype="multipart/form-data" 
target="upload_target" onsubmit="$('upload_progress').show();return true;">
    <input type="file" name="file" size="45"/>
    <input type="submit"/>
  </g:form>
  <iframe id="upload_target" name="upload_target" src="#" 
style="width:0;height:0;border:0px solid #fff;"></iframe>
</div>

...and the associated Grails controller method for receiving the file…

def save = {
    def file = request.getFile('file')
    if (file.originalFilename) {
        try {
            fileProcessingService.processImportFile(file)
            flash.error = null
            flash.message = "Import of '${file.originalFilename}' complete."
        }
        catch (Exception e) {
            flash.error = "There was an error importing '${file.originalFilename}':  
${e.message} (cause:${e.cause})"
            e.printStackTrace()
        }
    }
    else {
        flash.error = "Please specify a filename to upload."
    }
}

The fileProcessingService is a Spring injected service that does the actual parsing and loading of data into the database. However, one thing you might wonder is “if this is all happening in an IFRAME, how do the flash messages/errors get displayed outside of the IFRAME?” This is done via JavaScript that’s eval’d in the (automatically returned) save.gsp file.

<script language="javascript" type="text/javascript">
    window.top.window.stopUpload(${flash.error ? false : true},
"${flash.error ? flash.error.encodeAsHTML() : flash.message.encodeAsHTML()}");
</script>

...and the stopUpload(...); function looks like this (in index.gsp):

<script language="javascript" type="text/javascript">
  // called on return of the form from the IFRAME
  function stopUpload(success, msg) {
    // we're done uploading, so hide the progress indicator
    $('upload_progress').hide();
    // success is returned in the partial
    if (success) {
      // set the HTML to the message returned
      $('message').innerHTML = msg;
      $('message').show();
    }
    else {
      // set the HTML to the error returned
      $('errors').innerHTML = msg;
      $('errors').show();
    }
    return true;
  }
</script>

Hopefully most of the code + commentary makes this self explanatory. Comment if it’s not and I’ll edit the article.

Download “ajax-like” streaming works similarly with an IFRAME:

<a href="#" onclick="download('${createLink(action:"exportFile")}');">Export Data</a>
<iframe id="download_target" name="download_target" src="#" 
style="width:0;height:0;border:0px solid #fff;"></iframe>

...and the download(...) function in JavaScript…

<script language="javascript" type="text/javascript">
  function download(url) {
    $('download_target').src = url;
    // do AJAX stuff here, such as below
    // new Ajax.Updater('a_div_id','/a_url',{asynchronous:true,evalScripts:true});
  }
</script>

...and the exportFile Grails controller method…

def exportFile = {
    try {
        def results = fileProcessingService.createFileExport()
        if (results) {
            response.setContentType("text/plain")
            response.setHeader("Content-disposition", 
"attachment; filename=download-file.txt")
            response.outputStream << results
        }
        else {
            flash.message = 
"There was no data to export.  Please import a new file before exporting."
            render(template:'updateMessage')
        }
    }
    catch (Exception e) {
        flash.error = 
"There was an error exporting the download file:  ${e.message} (cause: ${e.cause})"
        e.printStackTrace()
        render(template:'updateMessage')
    }
}

...and finally the _updateMessage.gsp template (note the ’_’ indicating it’s a partial template):

${javascript(library:'prototype')}
<script type="text/javascript">
    <g:if test="${flash.message}">
        parent.document.getElementById('message').innerHTML = '${flash.message}'
        parent.document.getElementById('message').show();
    </g:if>
    <g:elseif test="${flash.error}">
        parent.document.getElementById('errors').innerHTML = '${flash.error}'
        parent.document.getElementById('errors').show();
    </g:elseif>
</script>

Note that this partial’s JavaScript uses a slightly different technique to directly access the parent page’s DOM to update the elements (rather than calling a JavaScript function in the parent page).

So there’s almost the entire coded needed to upload a file or download a file “AJAX-like” from and to a browser – shoot me questions or comments.

Comments are closed