File Sharing in Grails

Exclusive offer: get 50% off this eBook here
Grails 1.1 Web Application Development

Grails 1.1 Web Application Development — Save 50%

Reclaiming Productivity for faster Java Web Development

$23.99    $12.00
by Jon Dickinson | May 2009 | Java Open Source Web Development

Besides posting messages, our users also want to be able to upload files to share with other members of their team. Sounds good! But wait a minute, isn't file uploading a bit of a pain? First of all, we need to read the binary file data off the request, and then we need to figure out where to put the file. Do we store it on the file system, or in a database? Well, let's take a look.

In this article by Jon Dickinson, we will see how easy it is to manage file uploads and downloads in a Grails application.

File domain object

The first step, as usual, is to create a domain object to represent a file. We want to store the following information:

  • Name
  • The data of the file
  • A description of the file
  • The file size
  • Who uploaded the file
  • The date the file was created and last modified

Create the File domain class as follows:

package app
class File {
private static final int TEN_MEG_IN_BYTES = 1024*1024*10
byte[] data
String name
String description
int size
String extension
User user
Date dateCreated
Date lastUpdated
static constraints = {
data( nullable: false, minSize: 1, maxSize: TEN_MEG_IN_BYTES )
name( nullable: false, blank: false )
description( nullable: false, blank: false )
size( nullable: false )
extension( nullable: false )
user( nullable: false )
}
}

There should be nothing unfamiliar here. You have created a new domain class to represent a file. The file data will be stored in the data property. The other properties of the file are all metadata. Defining the user property creates the association to a user object. The constraints are then defined to make sure that all of the information that is needed for a file has been supplied.

There is one important side effect of setting the maxSize constraint on the data property. GORM will use this value as a hint when generating the database schema for the domain objects. For example, if this value is not specified, the underlying database may end up choosing a data type to store the binary file data that is too small for the size of files that you wish to persist.

FileController

Now, we will need a controller. Let's name it FileController. Our controller will allow users to perform the following actions:

  • Go to a page that allows users to select a file
  • Submit a file to the server
  • Download the file

Create the FileController groovy class, alongside our existing MessageController, by following the actions shown below:

package app
class FileController {
def create = {
return [ file: new File() ]
}
def save = {
}
def download = {
}
}

In the create action, we are simply constructing a new file instance that can be used as the backing object when rendering the file-upload form. We will fill in the implementation details of the save and download actions as and when we will need them.

File Upload GSP

The next step is to create a GSP to render the form that allows users to upload a file to the application. Create the file grails-app/views/file/create.gsp and enter the following markup:

<%@ page contentType="text/html;charset=UTF-8" %>
<html>
<head>
<meta http-equiv="Content-Type" content=
"text/html; charset=UTF-8"/>
<meta name="layout" content="main"/>
<title>Post File</title>
</head>
<body>
<g:hasErrors bean="${file}">
<div class="validationerror">
<g:renderErrors bean="${file}" as="list"/>
</div>
</g:hasErrors>
<g:form action="save" method="post" enctype="multipart/form-data"
class="inputform">
<fieldset>
<dl>
<dt>Title <span class="requiredfield">required</span></dt>
<dd><g:textField name="name" value="${file.name}"
size="35" class="largeinput"/></dd>
<dt>File <span class="requiredfield">required</span></dt>
<dd><input type="file" name="data"/></dd>
<dt>File description <span class="requiredfield">required</span></dt>
<dd><g:textArea name="description" value="${file
.description}" cols="40" rows="10"/></dd>
</dl>
</fieldset>
<g:submitButton name="Save" value="Save"/> |
<g:link controller="home">Cancel</g:link>
</g:form>
</body>
</html>

This GSP looks very similar to the create.gsp file for messages. Obviously, it has different fields that correspond to fields on the File domain class. The important difference is that this form tells the browser it will be submitting the file data:

<g:form action="save" method="post" enctype="multipart/form-data">

Run the application, go to http://localhost:8080/teamwork/file/create and sign in with the username flancelot and the password password. You should see the window as shown in the following screenshot:

Grails 1.1 Web Application Development

Saving the file

Now that our users can select files to upload, we need to implement the save action so that these files can be persisted and can be viewed by other users.

Grails file upload

Grails provides two methods of handling file upload. We are going to use both of them. The two approaches are:

  • Using data binding
  • Using the Spring MultipartFile interface

Data binding makes receiving the data of the file very simple, but is quite limited if used on its own. There is no way of binding anything other than the data of the file, such as the filename or the size of the file, to our domain object.

By also providing access to the Spring MultipartFile interface, Grails allows us to programmatically access any other information we might want from the file.

The save action

Update the FileController class and implement the save action as follows:

package app
import org.springframework.web.multipart.MultipartFile
class FileController {
def userService
def create = {
return [ file: new File() ]
}
def save = {
def file = new File( params )
file.user = userService.getAuthenticatedUser()
MultipartFile f = request.getFile( 'data' )
file.size = f.getSize() / 1024
file.extension = extractExtension( f )
if(file.save()) {
flash.userMessage = "File [${file.name}] has been uploaded."
redirect(controller: 'home')
} else {
render(view: 'create', model: [file: file])
}
}
def extractExtension( MultipartFile file ) {
String filename = file.getOriginalFilename()
return filename.substring(filename.lastIndexOf( "." ) + 1 )
}
def download = {
}
}

Apart from the implementation of the save action, we have had to import Spring MultipartFile and also inject the userService.

The first highlighted line within the save action performs the binding of request parameters to the File domain object. The usual binding will take place, that is, the name and the description properties of the File object will be populated from the request. In addition, since we have a property on our domain object that is an array of bytes, the contents of the file object in the request will also be bound into our File object.

A quick review of our code shows that we have the following property on theFile class:

byte[] data

Also the create.gsp defines the file input field with the same name:

<dd><input type="file" name="data" /></dd>

Grails is also capable of binding the contents of a file to a String property. In this case, we could just declare the data property in our File class as a String, and Grails would bind the file contents as a String.

The next line of interest occurs when we fetch the MultipartFile off the request by using the getFile method. We simply specify the request parameter that contains the file data and Grails does the rest. With an instance of MultipartFile we can access the file size and the original file name to extract the file extension.

Once we have finished populating our File object, we can call the save method and GORM will manage the persistence of the file object and the file data to the database.

Validation messages

The last thing we need to remember to add is the validation messages that will be displayed if the users don't enter all the data that is needed to save a file. Add the following to grails-app/i18n/messages.properties:

file.name.blank=You must give the file a name
file.description.blank=The file must have a description
file.data.minSize.notmet=No file has been uploaded
file.data.maxSize.exceeded=The file is too large. The maximum file size is 10MB
Grails 1.1 Web Application Development Reclaiming Productivity for faster Java Web Development
Published: May 2009
eBook Price: $23.99
Book Price: $39.99
See more
Select your format and quantity:

Viewing files

The next step is to show the uploaded files to the other team members using the application. We will display files on the home page next to messages. So we must:

  • Update the home page controller
  • Update the home page GSP
  • Add new message entries

The HomeController changes to:

package app
class HomeController {
def index = {
def messages = Message.list( sort: 'lastUpdated', order:
'desc', fetch: [ user: 'eager' ] )
def files = File.list( sort:'lastUpdated', order:'desc',
fetch:[user:'eager'] )
return [messages: messages, files: files]
}
}

A list of files is retrieved in exactly the same way as a list of messages. We then need to add the files to the model that is to be rendered.

In order to render the files, the home page GSP at grails-app/views/home/index.gsp must be updated. Add the following code after the message div:

<div class="panel">
<h2>Files</h2>
<g:each in="${files}" var="file">
<div class="afile">
<div class="filename">
<a href="${g.createLink(controller: 'file', action:
'download', id: file.id)}">
<g:message code="${file.name}" encodeAs="HTML"/>
</a> -
<g:message code="file.size" args="${[file.size]}"/>
<div class="filenamesupplimentary">
<g:message code="message.user"
args="${[file.user.firstName, file.user.lastName]}"/>
</div>
</div>
<div class="filebody">
<g:message code="${file.description}"
encodeAs="HTML"/>
</div>
</div>
</g:each>
</div>

The code above should look almost the same as the message list. The main difference is that the file name is wrapped by a link, which will allow a user to download the file by calling the download action on the file controller with the id of the current file. The implementation of the download action will follow shortly.

Add the following message to the message bundle (grails-app/i18n/messages.properties):

file.size={0} KB

Before running the application we also need to add a link to the home page that will allow our users to create new files. We can't have them enter the URL into the address bar all the time, can we? Update the newactions div on the grails-app/home/index.gsp file to be:

<div class="newactions">
<span>
<g:link controller="message" action="create">Post Message</g:link>
</span>
<span>
<g:link controller="file" action="create">Post File</g:link>
</span>
</div>

Now, run the application again, add a new file, and you should see a window that looks like the following screenshot:

Grails 1.1 Web Application Development

Modeling for efficiency

The implementation we have for file upload works fine and is easy to understand, but it is also quite naive in terms of efficiency. The problem is that every time a user views the information about a file, all the binary data for the file will be loaded into the memory. By altering our domain model slightly, we can easily improve the performance. We will need a domain class to represent the data as a one-to-one relationship with the File object. The relationships between domain objects are lazy by default. This means that, by extracting the data out into its own object, retrieving a list of files will not load the binary data of the file, unless we request it specifically.

Create a new domain class named FileData in the same package as the File domain class:

package app
class FileData {
private static final int TEN_MEG_IN_BYTES = 1024*1024*10
static belongsTo = [file:File]
byte[] data
static constraints = {
data( nullable: false, minSize: 1, maxSize: TEN_MEG_IN_BYTES )
}
}

The FileData class is defined as belonging to File because it is not possible for an instance of FileData to exist without an instance of File. By specifying that FileData belongs to File, we are saying that all persistence operations on file will cascade to FileData automatically. This means that when a file is saved the data object is saved. Similarly, when a file is deleted the data object will be deleted.

The File domain class must also be updated to replace the old byte[] data property with a reference to the new FileData class:

package app
class File {
String name
String description
int size
String extension
User user
FileData fileData
Date dateCreated
Date lastUpdated
static constraints = {
name( nullable: false, blank: false )
description( nullable: false, blank: false )
size( nullable: false )
extension( nullable: false )
user( nullable: false )
fileData( nullable: false )
}
}

We now need to change the save action on FileController and the file input field on the create.gsp file. The save action must be changed in the following manner:

def save = {
def file = new File( params )
file.user = userService.getAuthenticatedUser()
MultipartFile f = request.getFile( 'fileData.data' )
file.size = f.getSize() / 1024
file.extension = extractExtension( f )
if(file.save()) {
flash.userMessage = "File [${file.name}] has been uploaded."
redirect(controller: 'home')
} else {
render(view: 'create', model: [file: file])
}
}

While the file input element for create.gsp changes to:

<dd><input type="file" name="fileData.data"/></dd>

It is necessary to change the name of the input field to allow automatic binding of the submitted data to a new file instance. Supplying data on the request using dot notation allows Grails to bind request data into nested objects on the domain classes. For example, the File class has a property called fileData, which is a FileData instance. FileData has a property called data, which is an array of byte. By giving the file input field a name of fileData.data, we can be sure that the contents of the submitted file will bound to the data property of the object, which is assigned to the fileData property of our new File instance. All of this is handled through Grails data binding:

def file = new File( params )

Downloading files

The final piece of the puzzle is to allow users to download files that their teammates have posted on the site. We already have the link on the home page:

<a href="${g.createLink(controller:'file', action:'download', id:file.id)}">
<g:message code="${file.name}" encodeAs="HTML"/>
</a>

The File class needs to be given a downloadName property, which can be used for the filename when downloading. This is simply derived from the name given to the file and the file extension:

def getDownloadName() {
return "${name}.${extension}"
}

The download action already exists in the FileController; we just need to implement it, which is surprisingly simple:

def download = {
def file = File.get(params.id)
response.setContentType( "application-xdownload")
response.setHeader("Content-Disposition", "attachment;
filename=${file.downloadName}")
response.getOutputStream() << new ByteArrayInputStream( file.
fileData.data )
}

The first line uses the get method on File. This method is dynamically injected by the GORM to retrieve the instance of the class from the database with the specified ID. The next two lines set the necessary headers on the HttpServletResponse to tell the client that we are downloading a file with a given name. Finally, we create a ByteArrayInputStream from the data of our file and copy the file data to the response using the GDKs overloaded left shift operator (<<) on OutputStream.

As of Grails 1.1, it is possible to fetch instances of domain classes in read-only state by using the read method that is dynamically added to each domain class. This new operation would be appropriate for our download action as there should be no need to modify the file while downloading. If we used the read method instead of the get method, the first line of our download action would look like: def file = File.read(params.id).

If we were implementing file download in Java, we would need to either programmatically copy the data from our ByteArrayInputStream to the response OutputStream or find a utility class to do this for us.

Start up the application again and check that files can be downloaded now.

Summary

We have added support for file sharing to the teamwork application. This was incredibly easy due to the support that is built-in to Grails for handling file upload. We then used our knowledge of the default GORM behavior for the relationships to enable lazy loading of file binary data to reduce the memory footprint of the application.

Grails 1.1 Web Application Development Reclaiming Productivity for faster Java Web Development
Published: May 2009
eBook Price: $23.99
Book Price: $39.99
See more
Select your format and quantity:

About the Author :


Jon Dickinson

Jon Dickinson is the principal consultant and founder of Accolade Consulting Ltd. (http://www.accolade-consulting.co.uk) and can be contacted at jon@accolade-consulting.co.uk. He specializes in developing web applications on the Java platform to meet the goals of users in the simplest and least obtrusive way possible.

Books From Packt

 

Practical Plone 3: A Beginner's Guide to Building Powerful Websites
Practical Plone 3: A Beginner's Guide to Building Powerful Websites

WordPress Plugin Development: Beginner's Guide
WordPress Plugin Development: Beginner's Guide

Spring 2.5 Aspect Oriented Programming
Spring 2.5 Aspect Oriented Programming

Spring Web Flow 2 Web Development
Spring Web Flow 2 Web Development

Seam 2.x Web Development
Seam 2.x Web Development

Drools JBoss Rules 5.0 Developer's Guide
Drools JBoss Rules 5.0 Developer's Guide

Django 1.0 Website Development
Django 1.0 Website Development

Learning jQuery 1.3
Learning jQuery 1.3

 

Your rating: None Average: 5 (1 vote)
Helped me a lot, thanks so by
Helped me a lot, thanks so much !
Thanks by
Really Usefull

Post new comment

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
M
n
6
E
j
y
Enter the code without spaces and pay attention to upper/lower case.
Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software