Saturday, April 18, 2009

Google App Engine and File Upload

Google App Engine (GEA) finally got Java support. That's good. This is leaving me with no more excuses for delaying my Shogi Tools work.

So I got a grip and wrote some more code. Right now I am focusing on turn-based Shogi server.

I wanted there to provide the functionality to upload a user's picture.

But how to achieve this in GAE? GAE doesn't allow you to write files on the discs. How to do this fast and easy? I remember using in some of my earlier projects a ready to use Java library that made file uploading very easy for me.

Apache Commons FileUpload

Thank God for people from Apache.org. I went to their site http://commons.apache.org/fileupload/. The folks prepared a library to add robust, high-performance, file upload capability to our servlets and web applications. I decided to see how would it play with GAE.

Below there is a description of the process of creating a Java web application, under Eclipse environment, which uses commons FileUpload.


Example Google App Project
I assume you have downloaded and installed Google Plugin for Eclipse. In my case I used Eclipse 3.4 (and the dedicated plugin).

We'll start off with creating new Google App Engine project.

Open up your Eclipse. Then create new GAE project.
You can do this by clicking g+ icon on Eclipse toolbar () or choosing File->New->Other->Google->Web Application Project. "New Web Application Project" dialog shows up.


For the purpose of this "tutorial", provide the project name ("UploadTest"), package name ("org.fbc.uploadtest") and uncheck the GWT support. After clicking "Finish" GEA Eclipse
plugin creates a new project for us with a structure similar to the following screenshot:

HTML page for file upload
Next, we create a HTML file which enables the user to pick up the files to upload to the server.
Do this by replacing contents of (generated by GEA plugin) index.html. Open it for editing and paste the following content:


<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>GAE file upload test</title>
</head>
<body>
<form name="filesForm" action="uploadtest" method="post" enctype="multipart/form-data">
File 1:<input type="file" name="file1"><br>
File 2:<input type="file" name="file2"><br>
File 3:<input type="file" name="file3"><br>
<input type="submit" name="Submit" value="Upload Files">
</form>
</body>
</html>

There are few key points here:
  • form's attribute "method" is set to "post"
  • form's attribute "enctype" is set to "multipart/form-data"
  • form's "action" points to the servlet that will handle the upload (yet to be created)
  • input filed type is "file"


Configure Apache commons in Eclipse
First, if we haven't done it yet, we should download FileUpload and IO libraries from Apache's page. I decided to use the latest (1.2.1) version of FileUpload (http://archive.apache.org/dist/commons/fileupload/). The version is dependent (see http://commons.apache.org/fileupload/dependencies.html) on version 1.3.2 of IO library, so I downloaded exactly this version (http://archive.apache.org/dist/commons/io/binaries/commons-io-1.3.2-bin.tar.gz).

After unpacking the contents look for commons-fileupload-1.2.1.jar and commons-io-1.3.2.jar. Copy the files to war/WEB-INF/lib directory (see the project structure image above). This directory contains all the java libraries that will be deployed to appspot server for your app to use them.

Now, for your project to "see" them, you have to add the to the project's build path. I do this by right-clicking on the projects node in the project explorer, choosing Build Path->Configure Build Path (see the image below).


Then I go to "Library tab" (1), click Add JARs (2), indicate desired files in "war" directory (3) and add them to the build path (4). Look below.

The libraries are now in the project's classpath. We can start creating the servlet.


Upload servlet
We are ready to send the file. Now we must prepare ourselves to receive it on the server side. For that purpos we change the contents of "UploadTestServlet.java". GAE plugin generated this class for us in src folder and org.fbc.uploadtest package (see the picture with project structure).

Our upload form (index.html) says it sends the data to the server with "post" method. So, in our servlet we must provide doPost method. Here is the code:

public void doPost(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
try {
ServletFileUpload upload = new ServletFileUpload();
upload.setSizeMax(50000);
res.setContentType("text/plain");
PrintWriter out = res.getWriter();

try {
FileItemIterator iterator = upload.getItemIterator(req);
while (iterator.hasNext()) {
FileItemStream item = iterator.next();
InputStream in = item.openStream();

if (item.isFormField()) {
out.println("Got a form field: " + item.getFieldName());
} else {
String fieldName = item.getFieldName();
String fileName = item.getName();
String contentType = item.getContentType();

out.println("--------------");
out.println("fileName = " + fileName);
out.println("field name = " + fieldName);
out.println("contentType = " + contentType);

String fileContents = null;
try {
fileContents = IOUtils.toString(in);
out.println("lenght: " + fileContents.length());
out.println(fileContents);
} finally {
IOUtils.closeQuietly(in);
}

}
}
} catch (SizeLimitExceededException e) {
out.println("You exceeded the maximu size ("
+ e.getPermittedSize() + ") of the file ("
+ e.getActualSize() + ")");
}
} catch (Exception ex) {

throw new ServletException(ex);
}
}

The code should be quite self explanatory, so I won't dwell on this (although I am open to questions :-) ).

I'll just point out few things. I marked two places in the source bold.
The first line:
upload.setSizeMax(50000);

sets the file size limit. When the user tries to upload a file larger then (in our case) 50000 bytes, an exception occurs. When setting the maximal size remember about the GAE limits and quotas.

(If you paste doPost to the class, Eclipse should automatically add imports the code uses. If not so, do add the following lines manually:
import org.apache.commons.fileupload.FileItemIterator;
import org.apache.commons.fileupload.FileItemStream;
import org.apache.commons.fileupload.FileUploadBase.SizeLimitExceededException;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.IOUtils;
)

The second line
fileContents = IOUtils.toString(in);
does all the magic needed to fetch the input stream to String. With a single line of code. Thanks, IOUtils! ;-)

Now you have a String containing uploaded file's data waiting for you to do whatever you want to do with it.

In my example I just output the string to the browser (so when you test it it's best to use text files).


Testing the code
All is set and ready. Just run the code. I do this by right-clicking on the project root node in the project browser and choosing Run As->Web Application.


On the Eclipse console, if everything went fine, you should see the following message:

The server is running at http://localhost:8080/

So, open up your favourite browser and go to the address. Choose some files (remember the size limit) and submit the form.

In my case (I chose log4j.properties from src folder), I got the following message in my browser:

--------------
fileName = log4j.properties
field name = file2
contentType = application/octet-stream
lenght: 1063
# A default log4j configuration for log4j users.
#
# To use this configuration, deploy it into your application's WEB-INF/classes
# directory. You are also encouraged to edit it as you like.

# Configure the console as our one appender
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p [%c] - %m%n

# tighten logging on the DataNucleus Categories
log4j.category.DataNucleus.JDO=WARN, A1
log4j.category.DataNucleus.Persistence=WARN, A1
log4j.category.DataNucleus.Cache=WARN, A1
log4j.category.DataNucleus.MetaData=WARN, A1
log4j.category.DataNucleus.General=WARN, A1
log4j.category.DataNucleus.Utility=WARN, A1
log4j.category.DataNucleus.Transaction=WARN, A1
log4j.category.DataNucleus.Datastore=WARN, A1
log4j.category.DataNucleus.ClassLoading=WARN, A1
log4j.category.DataNucleus.Plugin=WARN, A1
log4j.category.DataNucleus.ValueGeneration=WARN, A1
log4j.category.DataNucleus.Enhancer=WARN, A1
log4j.category.DataNucleus.SchemaTool=WARN, A1

So, everything works fine. The browser uploaded a file to our application, which now could store the file in the GAE datastore.

Hope this info is helpful to anyone.

See you next time,
fat bold cyclop

P.S. I left working example on http://100.latest.kbguesttbook.appspot.com/.