October 17, 2007

Image Handling in Seam Apps part V: Restricting Access

This is the fifth 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 fifth installment shows you how to use Seam simplified security mode to restrict access to certain pages and actions. This simple yet powerful mode supports authentication and role based authorization.

If you use seam-gen to generate your projects then most of the work is done for you. Writing an authentication method and defining the roles are the only things you are left with. One way of defining the roles is to have database tables that represent members and roles in your application. The tutorial found here shows you how to define Member and MemberRole classes to serve this purpose.

Authentication

In the login page you need to have an inputText tag with a value binding set to identity.username, an inputSecret tag with a value binding set to identity.password and a commandButton tag with an action set to identity.login.


<h:form id="login-form">
<span class="error"><h:messages globalOnly="true"/></span>
<s:validateAll>
<fieldset>
<legend>Enter your login details</legend>
<div>
<h:outputLabel for="membername" value="Display name"/>
<h:inputText styleClass="input-field" id="membername"
value="#{identity.username}" size="40" maxlength="40"
required="true"/>
<h:message styleClass="error" for="membername"/>
</div>
<div>
<h:outputLabel for="password" value="Password"/>
<h:inputSecret styleClass="input-field" id="password"
value="#{identity.password}" size="40" maxlength="40"
required="true"/>
<h:message styleClass="error" for="password"/>
</div>
</fieldset>

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

The identity.login method propagates the credentials to the method specified in the security:identity tag defined in components.xml:

<security:identity authenticate-method="#{authenticator.authenticate}" />

The authenticate method validates the credentials and then retrieves a list of roles that member has. These roles are then added to the identity component.

@Name("authenticator")
public class Authenticator {
@Logger
Log log;

@In
Identity identity;

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

public boolean authenticate() {
log.info("authenticating #0", identity.getUsername());
String name = identity.getUsername();
if (name == null || name.trim().length() == 0)
return false;

String password = identity.getPassword();
String hash = null;
if (password != null && password.trim().length() != 0)
hash = DigestUtils.shaHex(identity.getPassword());
else
return false;
String query = "from gs_member m "
+ "WHERE m.memberName = :name AND m.passwordHash = :hash";
Object result = null;
try {
result = em.createQuery(query).setParameter("name", name)
.setParameter("hash", hash).getSingleResult();
} catch (NoResultException e) {
log.info("No member was found with name[#0]", name);
FacesMessages.instance().add("Could not find a member "
+ "with the given name and password.");
return false;
}
Member member = (Member) result;
if (member.getStatus().equals(MemberStatus.INACTIVE.getStatus())) {
FacesMessages.instance().add("Sorry but you need "
+ "to activate your account first. Please "
+ "follow the instructions in the email "
+ "you received at the time of registration.");
return false;
} else {
if (member.getRoles() != null) {
for (MemberRole role : member.getRoles())
identity.addRole(role.getName());
}
return true;
}
}
}

Authorization

The @Restrict annotation can be used to restrict access to methods based on a role. The #{s:hasRole('gsadmin')} expression delegates the call to identity.hasRole method passing it the values of gsadmin.

public class UploadService implements Upload {
@Restrict("#{s:hasRole('gsadmin')}")
public void upload() {
// process file upload
}
}

similarly you can define access restriction to pages in pages.xml

<page view-id="/upload.xhtml" login-required="true">
<restrict>#{s:hasRole('gsadmin')}</restrict>
</page>

The restrict tag has the same effect as the @Restrict annotation except that it applies to that specific page. An AuthorizationException is thrown in case a member who does not belong to the gsadmin role accesses the upload page. You can customize exception handling in pages.xml to perform a redirection with a custom error message.

As the name implies, the login-required indicates that only logged-in members are granted access to that specific page. If not yet logged-in, a redirection to the login page is performed and all page parameters are captured. After authenticating the member, another redirection takes place to show the original paeg. This is done with the help of two tags defined in components.xml:

<event type="org.jboss.seam.notLoggedIn">
<action expression="#{redirect.captureCurrentView}" />
</event>
<event type="org.jboss.seam.postAuthenticate">
<action expression="#{redirect.returnToCapturedView}" />
</event>


Calling Identity in Code

You can even apply access restriction inline by directly calling methods of the Identity interface. The the following example calls Identity.isLoggedIn to make sure that only logged-in members are allowed to view the artwroks:

public class ArtworkServlet extends javax.servlet.http.HttpServlet {

protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {

if(Identity.instance() == null || !Identity.instance().isLoggedIn()){
// show error message or redirect to home page
}

// code to view the artwork
}
}

I strongly recommend reading chapter 13 of Seam Documentation to learn more about the features and options provided to support Security in Seam.

October 16, 2007

Image Handling in Seam Apps part IV: Displaying and Caching

This is the fourth 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 fourth installment shows you how to use a Servlet to access seam components, load data from the database and view artworks in a web browser.

Seam Context Filter

Seam 2 provides a context filter that allows servlets to access component managed by Seam. This includes built in components, such as Identity, and user defined ones. This context filter needs to be configured in components.xml with a url pattern general enough to address all servlets not processed through the JSF lifecycle.


<web:context-filter url-pattern="/gallery/*" />

You define your servlets in web.xml using the usual tags servlet and servlet-mapping:

<servlet>
<servlet-name>Artwork Servlet</servlet-name>
<servlet-class>com.truerunes.web.ArtworkServlet</servlet-class>
<load-on-startup>2</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Artwork Servlet</servlet-name>
<url-pattern>/gallery/artworks/*</url-pattern>
</servlet-mapping>

The Artwork Servlet

This servlet services all requests to view artworks whether being initiated using an anchor tag, a h:graphicImage tag or accessed directly from the web browser. The example used in this tutorial has a request uri similar to this one: /truerunes/gallery/artworks/01.jpg. The first thing you need to do is to get the file name using the getPathInfo method of the request object:

public class ArtworkServlet extends javax.servlet.http.HttpServlet {

public static final long serialVersionUID = 596009789004L;

protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
String pathInfo = request.getPathInfo();
String fileName = WebUtil.getFileName(pathInfo);
if (fileName == null) {
String contextPath = request.getContextPath();
response.sendRedirect(contextPath + "/404.jsf");
return;
}
}
}

public class WebUtil {

public static String getFileName(String path) {
// Path format is "/file_name.jpg"
if (path != null && path.length() > 1) {
String fileName = path.substring(1);
int dotIndex = fileName.indexOf('.');
String extention = fileName.substring(dotIndex + 1);
if (extention.equals(Extention.jpg.getExtention())
|| extention.equals(Extention.png.getExtention())
|| extention.equals(Extention.gif.getExtention())) {
return fileName;
}
}
return null;
}

enum Extention {
jpg("jpg"), png("png"), gif("gif");

Extention(String extention) {
this.extention = extention;
}

private String extention;

String getExtention() {
return this.extention;
}
}
}

Now comes the beauty of Seam Context Filter, getting a component is as simple as calling Component.getInstance() with the name of the component you need. Here an entity manager and a user transaction are obtained using the getInstance method. Since an user managed transaction is obtaine you need to demarcate the boundaries of the transaction and account for any exceptions. The status of the transaction is verified agains Status.STATUS_ACTIVE to make sure that the transaction has not been started and the transaction manager did not begin the two-phase commit.

public class ArtworkServlet extends javax.servlet.http.HttpServlet {

protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {

// Thanks to Seam Context Filter, getting an entity manager is easy.
EntityManager em = (EntityManager) Component
.getInstance("entityManager");
UserTransaction utx = null;
boolean txStarted = false;
ArtworkInfo artworkInfo = null;
try {
utx = (UserTransaction) Component
.getInstance("org.jboss.seam.transaction.transaction");
if (utx.getStatus() != Status.STATUS_ACTIVE) {
utx.begin();
txStarted = true;
}
em.joinTransaction();
String query = "from gs_artwork_info a where a.fileName = :filename";
artworkInfo = (ArtworkInfo) em.createQuery(query).setParameter("filename",
fileName).getSingleResult();
if (txStarted)
utx.commit();
} catch (NoResultException nre) {
err.println("No artwork was found with name " + fileName);
} catch (Exception e) {
err.println("Exception when trying to read artwork " + fileName);
err.println("Exception message: " + e.getMessage());
e.printStackTrace();
try {
if (txStarted)
utx.rollback();
} catch (Exception ex) {
err.println("Rollback attempt failed: "+ex.getMessage());
ex.printStackTrace();
}
}
// calling em.close() causes "Exception calling @Destroy method."
}
}

Before streaming the artwork to the web browser, two important HTTP headers need to be set; Expires and Last-Modified. With these two header specified artworks will be cached locally in the web browser cache. This is crucial to avoid having to serve the same artwork each time a user decides to revisit a page that has an artwork. The Expires header is set to be a year from now and the Last-Modified header is set to the date the artwork got uploaded to the server.

public class ArtworkServlet extends javax.servlet.http.HttpServlet {

protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
if(artworkInfo == null){
err.println("Could not find an artwork with file name: "+fileName);
String contextPath = request.getContextPath();
response.sendRedirect(contextPath+"/404.jsf");
return;
}

Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
calendar.add(Calendar.YEAR, 1);
response.setDateHeader("Expires", calendar.getTimeInMillis());

calendar.setTime(artworkInfo.getUploadDate());
response.setDateHeader("Last-Modified", calendar.getTimeInMillis());

response.setContentType(artworkInfo.getContentType());
Artwork artwork = artworkInfo.getArtwork();
response.setContentLength((int)artwork.getSize());
response.getOutputStream().write(artwork.getData());
response.getOutputStream().flush();
response.getOutputStream().close();
}
}

October 15, 2007

Image Handling in Seam Apps Part III: Scaling

This is the third 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 third installment shows you how to scale uploaded artworks to generate thumbnails of a certain size. The method used to scale the artwork is called the Progressive Bilinear Scaling approach and is taking from Filthy Rich Clients. With this approach the artwork is scaled iteratively toward the final size, by exactly 50 percent each time until the final iteration.

When it comes to scaling images in Java, one would call either drawImage or getScaledInstance. The drawImage method has the advantage of being fast, but as the scale magnitude increases the quality drops significantly. The scaleImage method provides reasonable quality downscales when the scale factor is greater than half the original artwork size. On the other hand, the getScaledInstance provides the best quality for large downscales, however, the method has some performance implications. With getScaledInstance the performance difference involved can easily be in orders of magnitude. With the Progressive Bilinear approach you get a decent downscaled artwork in fraction of the time required by the getScaledInstance method. I would strongly recommend reading chapter four from the Filthy Rich Clients book.

The Code

The orginal scale method discussed in the book takes accepts and returns an instance of BufferedImage. I have update the method to account for the fact that an artwork are uploaded and stored as an array of bytes. The original scaling algorithm has not been modified though.


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

private byte[] data;

public void upload() {
byte[] thumbnailData = this.scale();
// see part ii of this tutorial for the complete method
}

private byte[] scale(){
InputStream inputStream = new ByteArrayInputStream(this.data);
BufferedImage uploadedImg = null;
try {
uploadedImg = ImageIO.read(inputStream);
} catch (IOException e) {
log.info("Could not craete a buffered image from the specified " +
"input stream: #0", e.getMessage());
e.printStackTrace();
}

int type = (uploadedImg.getTransparency() == Transparency.OPAQUE) ?
BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;

/*
* The uploaded image has to be passed to drawImage at the first
* iteration only. For the remaining iterations, the newly scaled image
* has to be passed instead. If you find this assignment a bit confusing
* you can always assign it a null and then add an if statement similar
* to the scratchImage below.
*/
BufferedImage scaledImage = (BufferedImage) uploadedImg;
BufferedImage scratchImage = null;
Graphics2D g2 = null;

int w = uploadedImg.getWidth();
int h = uploadedImg.getHeight();
int prevW = scaledImage.getWidth();
int prevH = scaledImage.getHeight();

// the default with of the thumbnail is set to 200
int targetWidth = 200;
double ratio = (double) targetWidth / w;
int targetHeight = (int) (h * ratio);

Object hint = RenderingHints.VALUE_INTERPOLATION_BILINEAR;

do {
if (w > targetWidth) {
w /= 2;
if (w < targetWidth) {
w = targetWidth;
}
}
if (h > targetHeight) {
h /= 2;
if (h < targetHeight) {
h = targetHeight;
}
}
if (scratchImage == null) {
// Use a single scratch buffer for all iterations
// and then copy to the final, correctly sized image
// before returning
scratchImage = new BufferedImage(w, h, type);
g2 = scratchImage.createGraphics();
}
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION , hint);
g2.drawImage(scaledImage, 0, 0, w, h, 0, 0, prevW, prevH, null);
prevW = w;
prevH = h;
scaledImage = scratchImage;
} while (w != targetWidth || h != targetHeight);
if (g2 != null) {
g2.dispose();
}
// If we used a scratch buffer that is larger than our
// target size, create an image of the right size and copy
// the results into it
if (targetWidth != scaledImage.getWidth() || targetHeight != scaledImage.getHeight()) {
scratchImage = new BufferedImage(targetWidth, targetHeight, type);
g2 = scratchImage.createGraphics();
g2.drawImage(scaledImage, 0, 0, null);
g2.dispose();
scaledImage = scratchImage;
}

String formatName = "";
if ("image/png".equals(contentType))
formatName = "png";
else if ("image/jpeg".equals(contentType))
formatName = "jpeg";

// convert the resulting image into an array of bytes
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
ImageIO.write(scaledImage, formatName, outputStream);
} catch (IOException e) {
log.info("Could not write the buffered image into the specified " +
"output stream: #0", e.getMessage());
e.printStackTrace();
}
return outputStream.toByteArray();
}
}

I scaled some artworks of different sizes, 480KB up to 15.7MB, and got these results:

Size Resolution Time
482 KB (1004x669) 270 ms
567 KB (1280x1024) 1523 ms
617 KB (640x904) 180 ms
747 KB (1024x768) 284 ms
856 KB (1400x1600) 1157 ms
995 KB (1606x1700) 723 ms
1313 KB (4673x3248) 30172 ms
1432 KB (2142x3018) 2311 ms
1539 KB (2142x3017) 1576 ms
1739 KB (2517x3200) 1863 ms
1903 KB (4504x2401) 2607 ms
2002 KB (1935x2708) 1300 ms
2320 KB (2399x3577) 2798 ms
2628 KB (2853x4020) 15772 ms
5.2 MB (1568x2256) 5475 ms
11.4 MB (3508x2500) 9443 ms
15.7 MB (3508x2500) 5600 ms

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

Image Handling in Seam Apps Part I: DB Model

This is the start 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

Introducing the example

The example used to demonstrate this tutorial allows a member to upload artworks of various sizes. The member has to login first and be a member of a predefined group. Once uploaded a thumbnail of the original artwork is generated on the fly. The original artwork and the generated image are then stored in the database. Other members can then view both of the thumbnail and the original artwork.

Database Schema

This first part of our tutorial introduces you the database schema and the entity beans representing the tables. To store the artwork and its thumbnail you need an additional table to store information shared between the two. This tables is called artwork_info and it has a one to one relationship with the other two tables. It also has a many to one relationship with the member table.



The following desc commands show the meta-data for them.



The data column in the gs_artwork table is defined as MEDIUMBLOB since a normal BLOB type can hold only up to 65,535 byte which is almost 64KB. You can store almost 16MB using a MEDIUMBLOB.

Programming Model

Each database table maps to its own Java class. There is a One-To-One Unidirectional relationship between the ArtworkInfo class and each of Artwork and Thumbnail. The Member class has a One-To-Many Bidirectional relationship with the ArtworkInfo class.



Let take a look at how we would mark up the ArtworkInfo class to implement the One-To-One relationship:


@Entity(name = "gs_artwork_info")
@Name("artworkInfo")
public class ArtworkInfo implements Serializable {

public static final long serialVersionUID = 596009789005L;

@Id
@GeneratedValue(strategy = AUTO)
@Column(name = "artwork_info_id")
private int id;

@NotNull
@Column(name = "file_name", unique = true)
private String fileName;

@NotNull
@Column(name = "content_type")
private String contentType;

@NotNull
@Column(name = "upload_date")
private Date uploadDate;

@NotNull
private String title;

@NotNull
@Column(name = "hit_count")
private int hitCount;

@OneToOne(fetch=FetchType.LAZY)
@JoinColumn(name = "artwork_id")
private Artwork artwork;

@OneToOne(fetch=FetchType.LAZY)
@JoinColumn(name = "thumbnail_id")
private Thumbnail thumbnail;

@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(name = "uploader")
private Member uploader;

// setters/getters omitted
}


The One-To-One relationship is defined using the @OneToOne annotation and is mapped using the @JoinColumn annotation. The @JoinColumn annotation defines the column in the ArtworkInfo's table that maps to join column, primary key in this case, of the Artwork's table. The @OneToOne annotation specifies the fetch type to be LAZY to avoid eagerly loading the artwork instance from the database. If you don't specify this property, your persistence provider would simply run a second query to retrieve all the columns of the Artwork's table when an instance of ArtworkInfo is read from the database.

To define the One-To-Many relationship between Member and ArtworkInfo you need to have an instance of Member annotated with @ManyToOne in the ArtworkInfo class. To access all the artworks uploaded by a member you need to define a collection-based relation property, Set<ArtworkInfo> in this case, with the @OneToMany annotation. The mappedBy property tells the persistence manager that the information for mapping this relationship to our tables is specified in the ArtworkInfo class, specifically to the uploader property of ArtworkInfo.


@Entity(name = "gs_member")
@Name("member")
public class Member implements Serializable {

public static final long serialVersionUID = 596009789001L;

@Id
@GeneratedValue(strategy = AUTO)
@Column(name = "member_id")
private Integer id;

@NotNull
@Length(max = 30, min = 2)
@Column(name = "member_name", unique = true)
private String memberName;

@OneToMany(mappedBy = "uploader", fetch = FetchType.LAZY)
private Set<ArtworkInfo> artworks;

// setters/getters omitted
}


The Artwork and Thumbnail classes are almost identical with minimal difference between the two. Since the data property stores the actual artwork in binary format you need to have the @Lob annotation. It informs the persistence manager that this property requires a lot of memory and that it needs to handle it internally as Blob. Here even though the data property is specified to be Lazily fetched, the persistence manager simply ignores this property; I have tried to use java.sql.Blob but even in this case the hint of lazily fetching the property was ignored.


@Entity(name = "gs_artwork")
@Name("artwork")
public class Artwork implements Serializable {

public static final long serialVersionUID = 596009789003L;

@Id
@GeneratedValue(strategy = AUTO)
@Column(name = "artwork_id")
private int id;

// the persistence manager ignores the fetch property.
@Lob
@Basic(fetch=FetchType.LAZY)
private byte[] data;

// setters/getters omitted
}

October 4, 2007

Seam Page Parameter with cutom Validators and Converters

I recently wrote about User Friendly Paging with Seam in which I used the @RequestParameter annotation. However, Gavin King has suggest that Seam page parameter be used rather than @RequestParameter. One advantage of using page parameter is that Seam set the values after performing appropriate type conversion. More important, "the <param> declaration is bidirectional." I would suggest reading Seam Documentation for more information.

The Components

Now that page parameters to be used, there is no need for the @RequestParameter and @out annotations, however, you need to define setter methods instead.


@Local
public interface MemberListing {
void setMemberList();
void setStatus(String status);
void setPageNumber(Integer pn);
void setPageSize(Integer ps);
}
@Stateless
@Name("memberListingService")
public class MemberListingService implements MemberListing{

private String status;
private String pn = null;
private String ps = null;

public void setStatus(String st){
status = st;
}

public void setPageNumber(Integer pageNumber){
pn = pageNumber.toString();
}

public void setPageSize(Integer pageSize){
ps = pageSize.toString();
}
}


Declaring the Page Parameters

For each page parameter, you need to declare a param tag with name and value attributes. Optionally, you can specify converter and validator as well.


<page view-id="/members.xhtml" action="#{memberListingService.setMemberList}">
<param name="st" value="#{memberListingService.status}"/>
<param name="pn" value="#{memberListingService.pageNumber}"
validator="#{pageNumberValidator}"/>
<param name="ps" value="#{memberListingService.pageSize}"
converter="#{pageSizeConverter}"/>
</page>


Custom Validator and Converter

Seams provides the @Validator and @Converter annotations to define components as custom JSF validators and converters. What is amazing about Seam is that you do not need to register these custom validators and converters with JSF. Seam takes care of that for you :)

The reason I am using these components here is because I want to display a custom error message for each parameter. If the standard converters/validators were used then the same error message would be displayed for parameters with same type.


@Name("pageNumberValidator")
@BypassInterceptors
@Validator
public class PageNumberValidator implements javax.faces.validator.Validator {

@Logger
private Log log;
public void validate(FacesContext context, UIComponent component,
Object value) throws ValidatorException {
log.info("PageNumberValidator.validate called");
try {
Integer pageNumber = Integer.parseInt(value.toString());
if (pageNumber < 1) {
FacesMessage message = new FacesMessage();
message.setDetail("Invalid page number [less than 1]");
message.setSummary("The page number cannot be "+pageNumber);
message.setSeverity(FacesMessage.SEVERITY_ERROR);
throw new ValidatorException(message);
}
} catch (NumberFormatException e) {
e.printStackTrace();
FacesMessage message = new FacesMessage();
message.setDetail("Invalid or missing page number");
message.setSummary("The page number has to be a valid number");
message.setSeverity(FacesMessage.SEVERITY_ERROR);
throw new ConverterException(message);
}
}
}


The validator here is annotated with Seam @Validator annotation and at the same time implements the Validator interface from the JSF API. The name use specify in the @Name annotation is the one used in the param tag above. To have a custom error message you define an instance of FacesMessage and set its properties accordingly. Then you need to pass it as a parameter to the newly thrown instance of ValidatorException. I know you are saying, this is too much for a custom error message but this is the only way I was able to find.

It would have been much simpler if the param tag had validatorMessage and convertorMessage attributes. I don't know but it might not be possible due to limitations in the JSF API.

Here is the code for the converter:

@Name("pageSizeConverter")
@BypassInterceptors
@Converter
public class PageSizeConverter implements javax.faces.convert.Converter {

@Logger
private Log log;

public Object getAsObject(FacesContext context, UIComponent component,
String value) throws ConverterException {
log.info("PageSizeConverter.getAsObject called");
if(value == null || value.trim().length() == 0)
return null;
Integer i = null;
try {
i = new Integer(value);
} catch (NumberFormatException e) {
e.printStackTrace();
FacesMessage message = new FacesMessage();
message.setDetail("Invalid page size [String specified]");
message.setSummary("The page size has to be a valid number");
message.setSeverity(FacesMessage.SEVERITY_ERROR);
throw new ConverterException(message);
}
return i.intValue();
}

public String getAsString(FacesContext context, UIComponent component,
Object value) throws ConverterException {
log.info("PageSizeConverter.getAsString called");
return value + "";
}
}


I totally forgot the screenshots


October 2, 2007

User Friendly Paging with Seam

Paging, also known as pagination, has many different implementations on the PHP universe. Developers just pick the one they like. However, there are not that many on the Java playground. You might have heard about the Value List Handler design pattern or used the Data Scroller component from the MyFaces project. Developers might find these approaches to be a bit complex or restrictive. Personally, I have read about them but never got the chance to use them.

With Hibernate and the Java Persistence API, the game has changed and Paging is much simpler now. Back in 2004, Gavin King blogged about a pattern to do Pagination in Hibernate and EJB3. The pattern is so simple and I really like it. The only thing that is remaining now is to integrate this pattern with Seam ;)

Update: Gavin King has suggest that Seam page parameter be used rather than @RequestParameter. Take a look here for using Seam page parameters with custom validators and converters.

Enhancing the pattern

The pattern Gavin introduced allows you to display "next" and "previous" buttons to the user and only retrieve the needed query results. This is nice and handy, however, users are impatient and get frustrated from repeatedly hitting the "next" and "previous" buttons to browse through a collection of items. I used to have a "browse" page with only these two buttons and one day I received an email from a user requesting that I offer pagination similar to the one seen on phpBB powered forums.




So, I have decided to do something similar but a bit different.

The Addons


Similar to phpBB pagination, the pattern discussed next has "first", "previous", "next", "last" and a list of links that the user can directly navigate two. As a developer you can specify the number of items to display per listing.



Notice that the total number of pages is displayed and users get to know which page they are currently browsing. In case there are not that many records in the database and there are only few pages to browse, page numbers are displayed rather than the verbose words.



The Components

To achieve this you two a component. One to validate data passed in the URL and to define the database queries. The second is responsible for identifying the format of the paging and the links to show.

The first component is a stateless session bean called MemberListingService that has a single method called setMemberList:


@Local
public interface MemberListing {
void setMemberList();
}

This method perform validation on the three parameters passed in the URL and set using the @RequestParameter annotation. If required, default values will be set for parameters that are missing or have invalid values.

@Stateless
@Name("memberListingService")
public class MemberListingService implements MemberListing {

// By default page number/index is NOT zero based. The first value is ONE.
private static int FIRST_PAGE_INDEX = 1;
private static String DEFAULT_STATUS = MemberStatus.ACTIVE.getStatus();

@RequestParameter("st")
@Out(scope = EVENT, value = "st", required = false)
private String status;

@RequestParameter("pn")
@Out(scope = EVENT, value = "pn", required = false)
private String pn = null;

@RequestParameter("ps")
@Out(scope = EVENT, value = "ps", required = false)
private String ps = null;

public void setMemberList() {
Integer pageIndex, pageSize = -1;

if (status == null || status.trim().equals("")) {
FacesMessages.instance().add("No status was specified.");
status = DEFAULT_STATUS;
}

if (pn == null || pn.trim().equals("")) {
FacesMessages.instance().add("No page number was specified.");
pageIndex = FIRST_PAGE_INDEX;
} else {
try {
pageIndex = Integer.parseInt(pn);
} catch (NumberFormatException e) {
FacesMessages.instance().add("invalid page number specified.");
pageIndex = FIRST_PAGE_INDEX;
}
}

if (ps == null || ps.trim().equals("")) {
FacesMessages.instance().add("No page size was specified.");
} else {
try {
pageSize = Integer.parseInt(ps);
} catch (NumberFormatException e) {
FacesMessages.instance().add("invalid page size specified.");
}
}
}
}

The next step is to create two Query objects. The first one is for the columns you want to show in you listing and the second is for getting a count of the records.

@Stateless
@Name("memberListingService")
public class MemberListingService implements MemberListing {

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

@Out(scope = EVENT, value = "page")
private Page page;

public void setMemberList() {

String selectStatement = "from gs_member m " + "WHERE m.status = :status "
+ "order by m.joinDate";
Query selectQuery = em.createQuery(selectStatement).setParameter("status",
status);

String countStatement = "SELECT COUNT(m) " + "FROM gs_member m "
+ "WHERE m.status = :status ";

Query countQuery = em.createQuery(countStatement).setParameter("status",
status);

this.page = new Page(selectQuery, countQuery, pageIndex, pageSize);
}
}

These two Query objects are then passed to a new instance of the Page class. If you execute the select query as is, you will retrieve all the records from the database. In the constructor of the Page class, further operations are applied to limit the list of records you get. The count query on the other hand will be executed as is without any further modifications.

public class Page {

private static int DEFAULT_PAGE_SIZE = 8;
private static int DEFAULT_STEP_SIZE = 2;
private static int MIN_PAGE_SIZE = 2;
private static int MAX_PAGE_SIZE = 16;

private List results;
private int pageIndex;
private int pageSize;
private int stepSize;
private int lastPageIndex;
private int leftSteps;
private int rightSteps;
private boolean leftDots;
private boolean rightDots;
private boolean simplePaging;

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

public Page(Query selectQuery, Query countQuery, int index, int size) {
if (index <= 0)
index = 1;

// If page index is not zero based
pageIndex = index;

// Make sure that the page size is within its limits
if (size < MIN_PAGE_SIZE || size > MAX_PAGE_SIZE)
pageSize = DEFAULT_PAGE_SIZE;
else
pageSize = size;

// You might consider having a min and max as boundaries
stepSize = DEFAULT_STEP_SIZE;

// Get the total number of items in the database.
// If count was of type Long you would lose the precision when
// calling Math.ceil() --> count is declared as double
double count = (Long) countQuery.getSingleResult();
if (count <= 0) {
// You need to handle this situation. One option is to throw an
// exception or display an error message using FacesMessages.
return;
}

// calculate the number of pages and set last page index
lastPageIndex = (int) Math.ceil(count / pageSize);

// make sure that the page index in not out of scope
if (pageIndex > lastPageIndex)
pageIndex = 1;

// calculate the minimum number of paging items to show
int minPagination = 3 + 2 * stepSize;
if (lastPageIndex <= minPagination) {
leftDots = rightDots = false;
simplePaging = true;
} else {
simplePaging = false;
setLeftDots();
setRightDots();
setLeftSteps();
setRightSteps();
}

results = selectQuery.setMaxResults(pageSize).setFirstResult(
pageSize * (pageIndex - 1)).getResultList();
}

private void setLeftDots() {
// current page - (step size + 1) > 1
// current page - step size > 1 + 1
// ex, current page = 5 and step size = 2
// output 1 ... 3 4 5
// 5 - 2 > 2 --> 3 > 2 --> true
leftDots = pageIndex > (stepSize + 2);
}

private void setRightDots() {
// current page + (step size + 1) < last page
// current page + step size < last page - 1
// ex, current page = 25, step size = 2 and last page = 29
// output 25 26 27 ... 29
// 25 + 2 < 29 - 1 --> 27 < 28 --> true
rightDots = (pageIndex + stepSize) < (lastPageIndex - 1);
}

private void setLeftSteps() {
if (leftDots)
leftSteps = stepSize;
else {
// count = current page - (first + 1)
// ex, first = 1 and page = 4
// count = 4 - (1 + 1) = 2
// output 1 2 3 4
leftSteps = pageIndex - 2;
}
}

private void setRightSteps() {
if (rightDots)
rightSteps = stepSize;
else {
// count = last page - ( current page + 1)
// ex, last = 29 and current = 26
// count = 29 - 27 = 2
// output 26 27 28 29
rightSteps = lastPageIndex - pageIndex - 1;
}
}

public boolean isLeftDots() {
return leftDots;
}

public boolean isRightDots() {
return rightDots;
}

public int getLeftSteps() {
return leftSteps;
}

public int getRightSteps() {
return rightSteps;
}

public List getResults() {
return results;
}

public int getPageIndex() {
return pageIndex;
}

public int getLastPageIndex() {
return lastPageIndex;
}

public boolean showPageIndex() {
return pageIndex != 1 && pageIndex != lastPageIndex;
}

public boolean isSimplePaging() {
return simplePaging;
}
}

I have added comments to the code since it's simpler to explain it that way, otherwise, I would end up with a long paragraph. Feel free to ask a question or a comment if you need further explanation. Note that certain links are displayed based on the current page






The members.xhtml page makes use of the "page" component outjected at the call to setMemberList is done. There is not much to explain here also so you just go through it and ask any question if something is not clear.

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:c="http://java.sun.com/jstl/core">

<ui:composition template="layout/main_template.xhtml">
<ui:define name="title">True Runes Member Listing</ui:define>
<ui:define name="content">
<!-- cellspacing is used to remove the default padding between cells -->
<h:dataTable cellspacing="0" id="memberListTable" value="#{page.results}"
var="mem" rowClasses="odd, even">
<caption>Members List</caption>
<h:column col="memberGroup">
<f:facet name="header">Member Name</f:facet>
<h:outputText value="#{mem.memberName}"/>
</h:column>
<h:column>
<f:facet name="header">Joined on</f:facet>
<h:outputText value="#{mem.joinDate}">
<f:convertDateTime type="date"/>
</h:outputText>
</h:column>
<h:column styleClass="last">
<f:facet name="header">Status</f:facet>
<h:outputText value="#{mem.status}"/>
</h:column>
</h:dataTable>

<c:choose>
<c:when test="#{page.simplePaging}">
<div id="paging">
<h:outputText value="Page #{page.pageIndex} of #{page.lastPageIndex} pages "
rendered="#{page.lastPageIndex > 1}"/>
<c:forEach var="i" begin="1" end="#{page.lastPageIndex}">
<h:outputLink value="members.seam">
<h:outputText value="#{i}"/>
<f:param name="st" value='#{st != null ? st : ""}'/>
<f:param name="ps" value='#{ps != null ? ps : 0}'/>
<f:param name="pn" value="#{i}" />
</h:outputLink>
</c:forEach>
</div>
</c:when>

<c:otherwise>
<div id="paging">
<h:outputText value="Page #{page.pageIndex} of #{page.lastPageIndex} pages "
rendered="#{page.lastPageIndex > 1}"/>

<h:outputLink value="members.seam" rendered="#{page.pageIndex != 1}">
<h:outputText value="First"/>
<f:param name="st" value='#{st != null ? st : ""}'/>
<f:param name="ps" value='#{ps != null ? ps : 0}'/>
<f:param name="pn" value="1" />
</h:outputLink>

<h:outputText value=" 1 " rendered="#{page.pageIndex == 1}"/>

<h:outputLink value="members.seam" rendered="#{page.pageIndex != 1}">
<h:outputText value="Previous"/>
<f:param name="st" value='#{st != null ? st : ""}'/>
<f:param name="ps" value='#{ps != null ? ps : 0}'/>
<f:param name="pn" value="#{page.pageIndex - 1}" />
</h:outputLink>

<h:outputText value=" ... " rendered="#{page.leftDots}"/>

<c:forEach var="i" begin="#{page.pageIndex - page.leftSteps}"
end="#{page.pageIndex - 1}">
<h:outputLink value="members.seam">
<h:outputText value="#{i}"/>
<f:param name="st" value='#{st != null ? st : ""}'/>
<f:param name="ps" value='#{ps != null ? ps : 0}'/>
<f:param name="pn" value="#{i}" />
</h:outputLink>
</c:forEach>

<h:outputText value=" #{page.pageIndex} "
rendered="#{page.showPageIndex()}"/>

<c:forEach var="i" begin="#{page.pageIndex + 1}"
end="#{page.pageIndex + page.rightSteps}">
<h:outputLink value="members.seam">
<h:outputText value="#{i}"/>
<f:param name="st" value='#{st != null ? st : ""}'/>
<f:param name="ps" value='#{ps != null ? ps : 0}'/>
<f:param name="pn" value="#{i}" />
</h:outputLink>
</c:forEach>

<h:outputText value=" ... " rendered="#{page.rightDots}"/>

<h:outputLink value="members.seam"
rendered="#{page.pageIndex != page.lastPageIndex}">
<h:outputText value="Next"/>
<f:param name="st" value='#{st != null ? st : ""}'/>
<f:param name="ps" value='#{ps != null ? ps : 0}'/>
<f:param name="pn" value="#{page.pageIndex + 1}" />
</h:outputLink>

<h:outputLink value="members.seam"
rendered="#{page.pageIndex != page.lastPageIndex}">
<h:outputText value="Last"/>
<f:param name="st" value='#{st != null ? st : ""}'/>
<f:param name="ps" value='#{ps != null ? ps : 0}'/>
<f:param name="pn" value="#{page.lastPageIndex}" />
</h:outputLink>

<h:outputText value=" #{page.lastPageIndex}"
rendered="#{page.pageIndex == page.lastPageIndex}"/>
</div>
</c:otherwise>
</c:choose>
</ui:define>
</ui:composition>
</html>

The last part of the buzzle is in pages.xml, you need to define setMemberList as a page action for the members.xhtml page.

<page view-id="/members.xhtml" action="#{memberListingService.setMemberList}"/>