October 14, 2007

Image Handling in Seam Apps Part II: File Upload

This is the second part of a tutorial about how to manage images in your Seam based web application. This tutorial consists of several parts:

Part One: The Database Model
Part Two: File Upload
Part Three: Scaling
Part Four: Displaying and Caching
Part Five: Restricting Access

This second installment shows you how to upload artworks using Seam built-in support for file upload. The uploaded artwork is then persisted in the database.

UPDATE: Thanks to Matt for pointing out the mistake I made and for suggesting the solution. The UploadService session been has been redefined as a Stateful session bean. It cannot be defined as a Stateless session bean since the EJB Container can swap an instance of a stateless session beans as soon as it finishes serving a method invocation. If you still want to use a Stateless session been then you need to pass all the information a method needs to know via parameters. You cannot assume that the same bean instance will serve all of your requests.

The User Interface

An authorized member selects a file from the local file system and sets a title for the artwork.



To upload files, Seam 2 has the fileUpload tag as part its tag library. This tag accepts a number of useful attributes which allow you to set the uploaded data in byte[] or InputStream, the file's content type, the name of the uploaded file and its size. You can also specify a list of accepted image types in case you only allow, say, PNG and JPEG images.


<h:form id="registration-form" enctype="multipart/form-data">
<span class="error"><h:messages globalOnly="true"/></span>
<s:validateAll>
<fieldset>
<legend>Enter file to upload</legend>
<div>
<h:outputLabel for="title" value="Title"/>
<h:inputText styleClass="input-field" id="title" required="true"
value="#{uploadService.title}" size="40" maxlength="40"/>
<h:message styleClass="error" for="title"/>
</div>
<div>
<h:outputLabel for="artwork" value="Artwork"/>
<s:fileUpload id="artwork" styleClass="input-field"
data="#{uploadService.data}" accept="image/*"
contentType="#{uploadService.contentType}"
fileName="#{uploadService.fileName}" fileSize="#{uploadService.size}"
size="40" maxlength="40" required="true"/>
<h:message styleClass="error" for="artwork"/>
</div>
</fieldset>

<div class="submit-buttons">
<h:commandButton value="Upload"
action="#{uploadService.upload}" id="registerButton"/>
</div>
</s:validateAll>
</h:form>

To handle multipart requests, you need to set the encoding type of the html form to multipart/form-data and to configure Seam Multipart Servlet Filter in web.xml.

<filter>
<filter-name>Seam Filter</filter-name>
<filter-class>org.jboss.seam.servlet.SeamFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>Seam Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

The Multipart Filter has two properties that can be set in components.xml: the first one offerers the option of storing the uploaded data in a temporary file rather than main memory while the second one offers setting a maximum size of uploads. It is recommended that these properties be set to avoid very large uploads and to free the memory resources.

<component class="org.jboss.seam.web.MultipartFilter">
<property name="createTempFiles">true</property>
<property name="maxRequestSize">4194304</property>
</component>


The Upload Service

Persisting the uploaded artwork is taking care in the UploadService stateless session bean. It has properties that can hold the values of the attributes specified in the fileUpload tag and and upload method that persists the artwork.

@Local
public interface Upload {
byte[] getData();
void setData(byte[] data);

String getContentType();
void setContentType(String contentType);

String getFileName();
void setFileName(String fileName);

String getTitle();
void setTitle(String title);

int getSize();
void setSize(int size);

void upload();
}

The first thing the upload method does is to get a scaled version of the artwork, scaling to be discussed in part three. It then gets an instance of the member who uploaded the artwork. After that the upload method creates instances of Thumbnail, Artwork and ArtworkInfo and call the persist method on each one of them.

@Stateful
@Name("uploadService")
public class UploadService implements Upload {

@Logger Log log;

@In(create = true, value = "entityManager")
private EntityManager em;

private byte[] data;
private String contentType;
private String fileName;
private String title;
private int size;

// setters/getters omitted

@Restrict("#{s:hasRole('gsadmin')}")
public void upload() {
byte[] thumbnailData = this.scale();

// Get the member early to avoid updating the info instance later.
Member uploader = (Member) em.createQuery(
"from gs_member m where m.memberName = :name").setParameter(
"name", Identity.instance().getUsername()).getSingleResult();

Thumbnail thumbnail = new Thumbnail();
thumbnail.setData(thumbnailData);
thumbnail.setSize(thumbnailData.length);
em.persist(thumbnail);

Artwork artwork = new Artwork();
artwork.setData(data);
artwork.setSize(size);
em.persist(artwork);

ArtworkInfo info = new ArtworkInfo();
info.setArtwork(artwork);
info.setThumbnail(thumbnail);
info.setContentType(contentType);
info.setFileName(fileName);
info.setHitCount(0);
info.setTitle(title);
info.setUploadDate(new Date());
info.setUploader(uploader);
em.persist(info);
}

@Remove
public void destroy(){}
}

Notice the call to the setUploader method, this call is required whenever you add or remove Member's ArtworkInfos. The relationship will not change in the database if you do not make this call. Also, to avoid executing extra database queries the uploader property is set before persisting the artwork info object.

Usually on production database servers there is a limit on the max packet size permitted, on MySQL the limit is 1MB. Keeping the limit would cause an exception to be thrown when an artwork greater than 1MB gets uploaded. To check for this value under Linux, you can run the following command:

shell> mysqladmin -u root -p variables | grep max_allowed_packet

Or the following command once you login into MySQL:

mysql> show variables like 'max%' ;

All you need to do is to set this value to a higher number. Under openSUSE 10.3 you need to change the MySQL configurations found in /etc/my.cnf, to be more specific you need to change line 31.

You can read more about the packet size limit here: http://dev.mysql.com/doc/refman/5.0/en/packet-too-large.html

8 comments:

Matt Drees said...

First, thanks for the tutorial; it looks handy.

I think you're using a Stateless bean when you should be using a Stateful one, though. Probably an event-scoped one.

Jay said...

Hi Matt,

Why should I use a Stateful bean to perform file uploads? Stateless beans are pooled, provide better performance and there is no overhead of passivation and activation. Moreover, no converstation state need to be maintained to perform file uploads

Matt Drees said...

I'll admit that I'm an EJB3 newbie.

But my understanding is that you can't depend on getting the same instance of a stateless bean when you invoke methods on it, so getters/setters are inherently unreliable.

And I agree you don't need conversational state, but you do need state within a single request. That's why I suggested an event-scoped stateful bean.

Jay said...

You are absolutely right Matt, I should have used Stateful session bean instead. As you said, a Stateless session been can be swapped as soon as it finished serving a method invocation so one cannot assume that the same instance will serve all the requests.

Anonymous said...

Thank you for this explaination on upload file with seam.
I want to now how you do to change the button in s:fileupload tag ? Could you help me please !
thanks.

Anonymous said...

You could have used a stateless session bean and saved yourself some work by not using getters and setters on your UploadService. You could have used your Artwork and ArtworkInfo entities on the input form and simply injected them into your UploadService.
data="#{artwork.data}"
fileSize="#{artwork.size}"
contentType="#{artworkInfo.contentType}"
fileName="#{artworkInfo.fileName}"
----
@In
Artwork artwork;
@In
ArtworkInfo artworkInfo;
-----

Jay said...

Interesting, I should have tried that.

Carey Foushee said...

How could you call this.scale() in your UploadService class when it doesn't extend any Image class?

Also I am getting a compiler error saying I need an @Destroy annotation as well as the @Remove.