Recently, Simon Brown put together a set of requirements for a very simple blogger application that could be used to compare Java web frameworks. I have my reservations about the actual requirememts he put together (in particular, there is no form submission!) but since some other framework authors have bitten, I've gone ahead and ported the example to Seam. I want to put a massive caveat around this post: Seam is absolutely not designed for applications like blogs or web forums; these kind of problems are very easy to solve using something like PHP or Ruby on Rails and there is no really good reason to use Java for a problem like this (unless Java is all you know). We have a set of requirements here with /no conversations/ and /no business processes/, so all the sophisticated state management machinery of Seam is redundant. Nevertheless, frameworks need to make simple things easy, and you'll see how little Java code we need to write to solve this simple problem using Seam.
To begin, I copied the standard Seam web.xml, faces-config.xml, application.xml and build.xml files from the Seam booking demo, changing names in a couple of places, and removing all the JSF navigation rules from faces-config.xml. None of this stuff is interesting, and it is always almost identical in every Seam application. I also copied Simon's screen.css stylesheet.
Simon started out with a domain model with Blog and BlogEntry classes that in a real application would be mapped to the database via Hibernate or EJB3, along with a BlogService class, which implements a static singleton containing some test data. The static variable and static initializer is an incredibly ugly way to implement a singleton in Seam, so I took the liberty of rewriting this class as an application-scope Seam component.
@Name("blog") @Startup @Scope(APPLICATION) public class BlogService { private Blog blog; @Create public void initBlog() { blog = new Blog(); blog.setName("Webapp framework blog"); blog.setDescription("Comparison of J2EE web application frameworks"); blog.setLocale(new Locale("en", "AU")); blog.setTimeZone(TimeZone.getTimeZone("PST")); blog.addBlogEntry(new BlogEntry(...); blog.addBlogEntry(new BlogEntry(...); blog.addBlogEntry(new BlogEntry(...); } @Unwrap public Blog getBlog() { return blog; } }
I left Blog and BlogEntry the way I found them and just copied them across.
That's all we need to start work on the first page of the web application.
There is some common header information on all pages of the application, so we'll use a facelets template, template.xhtml, to define the common stuff.
<?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" [=>xml:lang=]"en" lang="en"> <f:view> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>#{blog.name}</title> <link href="screen.css" rel="stylesheet" type="text/css" /> </head> <body> <div id="container"> <h1>#{blog.name}</h1> <h2>#{blog.description}</h2> <[=>ui:insert] name="content"/> </div> </body> </f:view> </html>
Note that this is all plain XML with namespaces, no wierd
tags.
The index page, index.xhtml, in the example application displays a list of the three latest blog entries.
<!DOCTYPE composition PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "[=>http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd]"> <[=>ui:composition] 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" template="template.xhtml"> <[=>ui:define] name="content"> <h:dataTable value="#{blog.recentBlogEntries}" var="blogEntry" rows="3"> <h:column> <div class="blogEntry"> <h3>#{blogEntry.title}</h3> <div> #{blogEntry.excerpt==null ? blogEntry.body : blogEntry.excerpt} </div> <p> <h:outputLink value="entry.seam" rendered="#{blogEntry.excerpt!=null}"> <f:param name="blogEntryId" value="#{blogEntry.id}"/> Read more </h:outputLink> </p> <p> Posted on <h:outputText value="#{blogEntry.date}"> <f:convertDateTime timeZone="#{blog.timeZone}" locale="#{blog.locale}" type="both"/> </h:outputText> </p> </div> </h:column> </h:dataTable> </[=>ui:define]> </[=>ui:composition]>
This is of course also plain XML.
(Usually I would not need to explicitly define locales in my templates. JSF and Seam use the request locale by default. But Simon's requirements say that I have to use the locale of the Blog.)
Now, if we hit http://localhost:8080/seam-blog/index.seam, this is the result:
https://hibernate.org/~gavin/blog_index.png
Now for the blog entry page (which we get to by clicking Read more
). Assuming I've understood
them correctly, Simon's requirements say that if a nonexistent entry is requested, we are supposed
to send a 404 error and forward to an error page. Now, this is not the most natural thing to do
in JSF or Seam. Usually, JSF applications use pull
-style MVC when handling GET requests.
Normally I would write the entry page to be able to handle the case of a nonexistent entry
(this is easy). But Simon is the boss here, and his requirements call for a push
-style
design. We'll use a Seam /page action/.
@Name("entryAction") public class EntryAction { @In private Blog blog; @In private FacesContext facesContext; @RequestParameter private String blogEntryId; @Out(scope=EVENT, required=false) private BlogEntry blogEntry; public void getBlogEntry() throws IOException { blogEntry = blog.getBlogEntry(blogEntryId); if (blogEntry==null) { HttpServletResponse response = (HttpServletResponse) facesContext.getExternalContext().getResponse(); response.sendError(HttpServletResponse.SC_NOT_FOUND); facesContext.responseComplete(); } } }
This action is meant to run before the entry page is rendered. It retrieves the requested BlogEntry from the singleton instance of Blog and outjects it to the event context. If no BlogEntry matches the request parameter, it sends a 404 error.
We need to list the page action in WEB-INF/pages.xml.
<pages> <page view-id="/entry.xhtml" action="#{entryAction.getBlogEntry}"/> </pages>
Now we can write the entry page, entry.xhtml:
<!DOCTYPE composition PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "[=>http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd]"> <[=>ui:composition] 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" template="template.xhtml"> <[=>ui:define] name="content"> <div class="blogEntry"> <h3>#{blogEntry.title}</h3> <div>#{blogEntry.body}</div> <p> Posted on <h:outputText value="#{blogEntry.date}"> <f:convertDateTime timezone="#{blog.timeZone}" locale="#{blog.locale}" type="both"/> </h:outputText> </p> </div> </[=>ui:define]> </[=>ui:composition]>
Along with the 404 error page, 404.xhtml:
<!DOCTYPE composition PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "[=>http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd]"> <[=>ui:composition] 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" template="template.xhtml"> <[=>ui:define] name="content"> <h3>Page not found</h3> </[=>ui:define]> </[=>ui:composition]>
This page needs to be listed in web.xml:
<error-page> <error-code>404</error-code> <location>/404.seam</location> </error-page>
Now if I click the Read more
link, I get to the URL
http://localhost:8080/seam-blog/entry.seam?blogEntryId=3
and see the following:
https://hibernate.org/~gavin/blog_entry.png
If I edit the URL and change the id to 6, I'll get a 404:
https://hibernate.org/~gavin/blog_404.png
And so we're done :-)