AJAX-like File Upload and Download in Grails
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.
Deploying Rails 2.0.2 on Tomcat 5.5.x with Warbler
After working with Rails for some time I wanted to determine how to deploy an application on Tomcat. I will discuss how to build a war file that can be used on Tomcat and show pitfalls I experienced along the way. Here are the technologies I am using for this deployment:
- JRuby 1.1.1
- Rails 2.0.2
- Warbler 0.9.5
- MySQL 5.0.4
To package up my Rails application I am using Warbler as indicated above. This is an alternative to Goldspike but allows a better packaging scheme and an easier configuration. Warble is actually a wrapper to Goldspike and includes these dependencies when you install the gem. You can read more about Warbler here.
I will assume you have Rails installed as a gem in JRuby. To make a standalone war file make sure you run this command to freeze Rails in your project:
$ jruby -S rail:freeze:gems
This will write the standard Rails gem information to your RAILS_ROOT/vendor/rails directory. Here is how you install Warbler from JRuby:
$ jruby -S gem install warble
After this is configured switch to the root of your Rails project and execute this command:
$ jruby -S warble
This will build an exploded war file in a temporary directory within your project. This path can be changed in the warble.rb file that gets installed. Now create a Warble configuration file by executing this command since we will need it (not sure why this is done after the fact):
$ jruby -S warble config
After generating a configuration file you should consider updating the JRuby jar file that comes with Warble since you will want the latest version packaged with your war file. If you do not do this Warble will always include the original version of the JRuby jar with your war file. Add the jruby-complete-1.1.1.jar file which can be obtained here) to your RAILS_HOME/lib directory. Grab the latest Goldspike jar file and place it in your RAILS_HOME/lib directory. Now add this code to the configuration:
config.java_libs.reject! {
|lib| lib =~ /jruby-complete|goldspike/
}
This allows you to externally package your own versions of JRuby and Goldspike and reject the verions of these jar files that come bundled in the gem directory ($JRUBY_HOME/lib/ruby/gems/1.8/gems/warbler-0.9.5/lib/warbler).
Also, you can delete the exploded directory and the war file by issuing this command:
$ jruby -S warble war:clean
From this point you just move the war file to your Tomcat webapps directory and start the server. This is great when everything works. However, I was lucky enough to run into some issues while making this happen.
The first issue occurred when I started Tomcat with the war file I built with Warble (remember that this is using Goldspike under the covers) and got this error message in the Tomcat logs:
“Could not load Rails. See the logs for more details.”
Yipee! So when I went to look in the Tomcat logs there was no additional information. Great! It turns out that the stack trace output gets swallowed on a Mac. This is fairly easy to fix (but should be patched by the Goldspike team) by modifying the source code for the goldspike-1.6.1.jar file to emit the full stack trace. Grab the latest Goldspike code from here:
svn checkout
svn://rubyforge.org/var/svn/jruby-extras/trunk/rails-integration
You will have to modify src/main/java/org/jruby/webapp/RailsFactory.java like this:
} catch (RaiseException e) {
//added this line so the full stack trace is shown
e.printStackTrace();
logRubyException("Failed to load Rails", e);
throw new ServletException(
"Could not load Rails. See the logs for more details.");
}
After you rebuild the Goldspike jar add it to your RAILS_ROOT/lib. Now re-warble your war file and put it on your Tomcat server. Now you should be able to find your error. In my case I was not making a connection to my database. I was clearly missing gems in my war file. Great!
This really is not too big of a deal. You can explain to Warbler what gems need to be packaged with your war file. Here is a snippet from my warble.rb file that shows how I included the missing gems:
# Gems to be packaged in the webapp. Note that Rails
# gems are added to this list if vendor/rails is not
# present, so be sure to include rails if you overwrite
# the value
config.gems = ["activerecord-jdbc-adapter", "jruby-openssl", "activerecord-jdbcmysql-adapter", "jdbc-mysql"]
Alright re-warble and... still no connectivity. Awesome! This time I was missing my MySQL jar file in my war file and the activerecord-jdbcmysql-adapter gem could not communicate with the implementation jar. To fix this copy your MySQL jar file to the RAILS_ROOT/lib directory in your Rails project.
One more re-warbling and more good news:
“Rails Error: No :secret given to the #protect_from_forgery call. Set that or use a session store capable of generating its own keys (Cookie Session Store)”.
This is a known bug with Goldspike and Rails 2.0. So edit your web.xml file and add the following:
<context-param>
<param-name>jruby.session_store</param-name>
<!-- This value really means let Rails take care
of session store -->
<param-value>db</param-value>
</context-param>
OK, please just one more re-warbling and deployment to Tomcat and a successful standalone war file has been created and deployed. *birds singing*
I would also suggest increasing the Java memory setting in the JAVA_OPTS of Tomcat to at least -Xmx512m to improve performance. Also keep in mind that Warble is configured to use the production environment for your Rails application so make the appropriate migrations before starting the application. You can make further modification to the Goldspike configuration within the warble.rb file to tweak the pool of Rails runtimes. Once I made these changes it was pretty transparent where I was running my Rails application.
I hope this saves other people time when running into issues. This was kind of a frustrating process since it should be a simple one line deployment. However, I was happy with the end results.
Migrating to Grails?
The last time I looked at Grails it was around version 0.4. Now that we are approaching a 1.0 release I wanted to see how much further the Grails team has advanced. I wanted to start by using my existing Spring and Hibernate components from my Java project (I am currently using Stripes for our presentation layer). Benefits for doing this might include:
- Abstracting your Java domain model and services so they are not coupled to Grails.
- The ability to continue using your Java services and model objects outside of Web applications or have the ability to integrate with other Web presentation frameworks.
- A migration path to Groovy if you want to rid yourself of straight Java altogether. I am sure that there would be massive redux in LOC. For me it was experimenting with the intersection of my experiences with Rails and Java Web development.
Does this mean that if you reuse your Hibernate Java domain objects you do not get all of the GORM features like (order.save())? No, your domain objects transparently take full advantage of the mixed in methods in Grails. You might find that the services you are reusing from Spring are mostly passthroughs to DAO that you do not need anymore because of GORM. Here is a trivial example showing how to use a Grails controller to use existing Spring services to perform DAO operations and how to use your Hibernate domain objects directly as GORM objects:
UserService userService // This is injected in via auto-wiring
def index = {
// Using Java Services
def user = userService.find(1, User.class)
println user.firstName
user.firstName = 'Mark'
userService.save user
println User.get(1).firstName
// Using strait Grails
user = User.get(1)
println user.firstName
user.firstName = 'Phil'
user.save()
println User.get(1).firstName
}
Here is how we took our annotated Hiberate classes so Grails would recognize them:
Edit your hibernate.cfg.xml file in the GRAILS_APP/grails_app/conf/hibernate directory and add your domain objects:
<!DOCTYPE hibernate-configuration SYSTEM
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<mapping class="com.meagle.common.bo.Address" />
<mapping class="com.meagle.common.bo.User" />
</session-factory>
</hibernate-configuration>
In your DataSource.groovy configuration file add the configClass line to your datasource configuration so your annotated Hibernate classes will be recognized by Grails:
dataSource {
pooled = false
driverClassName = "com.ibm.db2.jcc.DB2Driver"
username = "meagle"
password = "******"
configClass
=org.codehaus.groovy.grails.orm.hibernate.cfg.GrailsAnnotationConfiguration.class
}
Here is how you expose your existing Spring beans to your Grails application:
- Edit your GRAILS_APP/grails_app/conf/spring/resources.xml file and add the following line:
<import resource="applicationContext-core.xml"/>
This tells Grails to include your Spring resources (We put our Spring config file in the same directory as the resources.xml file).
- Make sure all of your Spring and Hibernate domain objects written in Java are on the classpath. We put ours in a jar file in the GRAILS_APP/lib directory.
One gotcha here to remember is that your Spring configuration file should not include any definition to a SessionFactory and your services and DAOs that reference a SessionFactory needs to be the name "sessionFactory". Otherwise, you will end up with multiple session factory objects which is probably not what you want.
Overall I am very impressed with Grails at this point and would challenge Java developers to take a serious look at this framework. If you are fortunate enough to have IntelliJ 7 then there is decent plugin support for building Grails applications.
