Java Web Application Development With Click Framework/Pages
Pages are the heart of web applications. In Click, Pages encapsulate the processing of HTML requests and the rendering of HTML responses. The section discusses Click pages and covers to following topics:
- Page Java classes
- Page execution sequence
- Request parameter to page field auto binding
- Navigation between pages
- Templating common page content
- Page security model
- Stateful pages
- Page error handling
- Page not found handling
- Pages message properties
In Click, a logical page is composed of a Java class and a Velocity template, with these components being defined in page elements of the click.xml file.
<page path="search.htm" classname="com.mycorp.page.Search"/>
The path attribute specifies the location of the page Velocity template, and the classname attribute specifies the page Java class name.
Alternatively you can also configure Click to use JSP pages for rendering.
<page path="search.jsp" classname="com.mycorp.page.Search"/>
Classes
[edit | edit source]All custom Click pages must subclass the [click-api/net/sf/click/Page.html Page] base class. The Page class and its associated companion classes, Context and Control, are depicted below in Figure 1.
Figure 1. Page Class Diagram - created with Enterprise Architect courtesy Sparx Systems
The Page class provides a model attribute which is used to hold all the objects that are rendered in the page's Velocity template. The model may also contain Control objects, which provide user interface controls on the Page.
Pages also have an associated Context object which references all the javax.servlet objects associated with the request. When programming in Click you use the Context object to access HttpServletRequest attributes, parameters and the HttpSession object.
The Page class provide a number of empty handler methods which subclasses can override to provide functionality:
- onSecurityCheck()
- onInit()
- onGet()
- onPost()
- onRender()
- onDestroy()
The ClickServlet relies on instantiating Pages using a public no arguments constructor, so when you create Page subclasses you must ensure you don't add an incompatible constructor.
Execution
[edit | edit source]The GET request execution sequence for Pages is summarized below in the Figure 2.
Figure 2. GET Request Sequence Diagram - created with Enterprise Architect courtesy Sparx Systems.
Stepping through this GET request sequence, a new Page instance is created and the attributes for the Page are set (context, format, headers, path). Next request parameter values are then bound to any matching public Page fields.
Then the onSecurityCheck() handler. This method can be used to ensure the user is authorized to access the page, and if necessary can abort any further processing.
The next method invoked is the onInit(), this is where you place any post constructor initialization code.
The next step is the processing of the Page's controls. The ClickSerlvet gets the list of Controls from the page and then iterates through the list calling onProcess(). If any of the Control's onProcess() methods return false, processing of subsequent controls and the Page's onGet() method is aborted.
If everything is executing normally the Page's onGet() method is now called.
The next step is rendering the page template to generate the displayed HTML. The ClickServlet gets the model (Map) from the Page then adds the following objects to the model:
- any public Page fields using the field's name
- context - the Servlet context path, e.g. /mycorp
- cssImports - the CSS imports and style blocks to include in the page's header.
- format - the Format object for formatting the display of objects.
- imports - the CSS and JavaScript imports to include in the pages header.
- jsImports - the JavaScript imports and script blocks to include in the page's footer.
- messages - the MessagesMap adapter for the Page getMessage() method
- path - the path of the page template to render
- request - the page's HttpServletRequest object
- response - the page's HttpServletResponse object
- session - the SessionMap adapter for the user's HttpSession
It then merges the template with the page model and writes out results to the HttpServletResponse. When the model is being merged with the template, any Controls in the model may be rendered using their toString() method.
The final step in this sequence is invoking the Page's onDestroy() method. This method can be used to clean up resource associated with the Page before it is garbage collected. The onDestroy() method is guaranteed to be called even if an exception occurs in the previous steps.
The execution sequence for POST requests is almost identical, except the onPost() method is invoked instead on onGet(). See the [../images/post-sequence-diagram.png POST Request Sequence Diagram].
Another view on the execution flow of Pages is illustrated in the Activity diagram below.
Figure 3. Page Execution Activity Diagram - created with Enterprise Architect courtesy Sparx Systems.
Request Param Auto Binding
[edit | edit source]Click will automatically bind any request parameter values to public Page fields with the same name. When binding these values it will also attempt to convert them to the correct type.
The best way to understand this is to walk through an example. Our application receives a GET request:
http://localhost:8080/mycorp/customer-details.htm?customerId=7203
This request is automatically handled by our CustomerDetails page:
package com.mycorp.page;
public class CustomerDetails extends Page
{
public Integer customerId;
}
After the CustomerDetails page has been created the "customerId" request parameter value "7023" will be converted into an Integer and assigned to the public page field customerId.
Another feature of Click is that any public Page fields are automatically added to the page's model before it is rendered. This will make these values available in the page template for display. In our example the public customerId field will be added to the Page model and will be available for rendering in the page template:
Our customer-details.htm page template contains:
<html>
<body>
Customer ID: $customerId
</body>
</html>
After processing the request our page would be rendered as:
- Customer ID: 7203
Customizing Auto Binding
[edit | edit source]Auto binding supports the conversion of request string parameters into the Java classes: Integer, Double, Boolean, Byte, Character, Short, Long, Float, BigInteger, BigDecimal, String and the various Date classes.
By default type conversion is performed by the [click-api/net/sf/click/util/RequestTypeConverter.html RequestTypeConverter] class which is used by the ClickServlet method [click-api/net/sf/click/ClickServlet.html#getTypeConverter() getTypeConverter()].
If you need to add support for additional types, you would write your own type converter class and subclass the ClickSerlvet to use your custom converter.
For example if we wanted to automatically load a Customer object from the database when a customer id request parameter is specified, you could write your own type converter:
public class CustomTypeConverter extends RequestTypeConverter
{
private CustomerService customerService = new CustomerService();
/**
* @see RequestTypeConverter#convertValue(Object, Class)
*/
protected Object convertValue(Object value, Class toType)
{
if (toType == Customer.class)
{
return customerService.getCustomerForId(value);
}
else
{
return super.convertValue(value, toType);
}
}
}
This type converter would handle the following request:
http://localhost:8080/mycorp/customer-details.htm?customer=7203
Loading the customer object from the database using given the "7203" customer id value. The ClickServlet would then assign this customer object to the matching page field:
package com.mycorp.page;
public class CustomerDetails extends Page
{
public Customer customer;
}
To make your custom type converter available you will need to subclass ClickServlet and override the getTypeConverter() method. For example:
public class CustomClickServlet extends ClickServlet
{
/**
* @see ClickServlet#getTypeConverter()
*/
protected TypeConverter getTypeConverter()
{
if (typeConverter == null)
{
typeConverter = new CustomTypeConverter();
}
return typeConverter;
}
}
Navigation
[edit | edit source]Navigation between pages is achieved by using forwards, redirects and by setting the page template path.
Forward
[edit | edit source]To forward to another page using the servlet RequestDispatcher set the Page's forward property. For example to forward to a page with a path index.htm:
/**
* @see Page#onPost()
*/
public void onPost()
{
// Process form post
..
setForward("index.htm");
}
This will invoke a new Page class instance mapped to the path index.htm. Note when a request is forwarded to another Page, the controls on the second page will not be processed. This prevents confusion and bugs, like a form on the second page trying to process a POST request from the first page.
Forward Parameter Passing
[edit | edit source]When you forward to another page the request parameters are maintained. This is a handy way of passing through state information through with the request. For example to you could add a customer object as request parameter which is displayed in the template of the forwarded page.
public boolean onViewClick()
{
Long id = viewLink.getValueLong();
Customer customer = CustomerDAO.findByPK(id);
getContext().setRequestAttribute("customer", customer);
setForward("view-customer.htm");
return false;
}
Forwarded to page template view-customer.htm:
<html>
<head>
<title>Customer Details</title>
</head>
<body>
<h1>Customer Details</h1>
<pre>
Full Name: $customer.fullName
Email: $customer.email
Telephone: $customer.telephone
</pre>
</body>
</html>
Request attributes are automatically added to the Velocity Context object so are available in the page template.
Page Forwarding
[edit | edit source]Page forwarding is another way of passing information between pages. In this case you create the page to be forwarded to using the Context createPage() method and then set properties directly on the Page. Finally set this page as the page to forward the request to. For example:
public boolean onEditClick()
{
Long id = viewLink.getValueLong();
Customer customer = CustomerDAO.findByPK(id);
EditPage editPage = (EditPage) getContext().createPage("/edit-customer.htm");
editPage.setCustomer(customer);
setForward(editPage);
return false;
}
When creating a page with the createPage() method ensure you prefix the page path with the "/" character.
You can also specify the target page using its class as long as the Page has a unique path. Using this technique the above code becomes:
public boolean onEditClick()
{
Long id = viewLink.getValueLong();
Customer customer = CustomerDAO.findByPK(id);
EditPage editPage = (EditPage) getContext().createPage(EditPage.class);
editPage.setCustomer(customer);
setForward(editPage);
return false;
}
This Page forwarding technique is best practice as it provides you with compile time safety and alleviates you from having to specify page paths in your code.
Please always use the Context createPage() methods to allow Click to inject Page dependencies.
Template Path
[edit | edit source]An alternative to forwarding to a new page is to simply set the path to the new page template to render. With this approach the page template being rendered must have everything it needs without having its associated Page object being created. Our modified example would be:
public boolean onViewClick()
{
Long id = viewLink.getValueLong();
Customer customer = CustomerDAO.findByPK(id);
addModel("customer", customer);
setPath("view-customer.htm");
return false;
}
Note how the customer object is passed through to the template in the Page model. This approach of using the Page model is not available when you forward to another Page, as the first Page object is "destroyed" before the second Page object is created and any model values would be lost.
Redirect
[edit | edit source]Redirects are another very useful way to navigate between pages.
The great thing about redirects are that they provide a clean URL in the user's browser which matches the page that they are viewing. This is important for when users want to bookmark a page. The downside of redirects are that they involve a communications round trip with the users browser which requests the new page. Not only does this take time, it also means that all the page and request information is lost.
An example of a redirect to a logout.htm page is provided below:
public boolean onLogoutClick()
{
setRedirect("/logout.htm");
return false;
}
If the redirect location is begins with a "/" character the redirect location will be prefixed with the web application's context path.
For example if an application is deployed to the context "mycorp" calling setRedirect("/customer/details.htm") will redirect the request to: "/mycorp/customer/details.htm"
You can also obtain the redirect path via the target Page's class. For example:
public boolean onLogoutClick()
{
String path = getContext().getPagePath(Logout.class);
setRedirect(path);
return false;
}
Note when using this redirect method, the target Page class must have a unique path.
A short hand way of redirecting is to simply specify the target Page class in the redirect method. For example:
public boolean onLogoutClick()
{
setRedirect(Logout.class);
return false;
}
Redirect Parameter Passing
[edit | edit source]You can pass information between redirected pages using URL request parameters. The ClickServlet will encode the URL for you using HttpServletResponse.encodeRedirectURL(url) method.
In the example below a user will click on an OK button to confirm a payment. The onOkClick() button handler processes the payment, gets the payment transaction id, and then redirects to the trans-complete.htm page with the transaction id encoded in the URL.
public class Payment extends Page
{
..
public boolean onOkClick()
{
if (form.isValid())
{
// Process payment
..
// Get transaction id
Long transId = OrderDAO.purchase(order);
setRedirect("trans-complete.htm?transId=" transId);
return false;
}
return true;
}
}
The Page class for the trans-complete.htm page can then get the transaction id through the request parameter "transId":
public class TransComplete extends Page
{
/**
* @see Page#onInit()
*/
public void onInit()
{
String transId = getContext().getRequest().getParameter("transId");
if (transId != null)
{
// Get order details
Order order = OrderDAO.findOrderByPK(new Long(transId));
if (order != null)
{
addModel("order", order);
}
}
}
}
Post Redirect
[edit | edit source]The parameter passing example above is also an example of a Post Redirect. The Post Redirect technique is a very useful method of preventing users from submitting a form twice by hitting the refresh button.
Page Templating
[edit | edit source]Click supports page templating (a.k.a. Tiles in Struts) enabling you to create a standardized look and feel for your web application and greatly reducing the amount of HTML you need to maintain.
To implement templating define a border template base Page which content Pages should extend. The template base Page class overrides the Page getTemplate() method, returning the path of the border template to render. For example:
public class BorderedPage extends Page
{
/**
* @see Page#getTemplate()
*/
public String getTemplate()
{
return "/border.htm";
}
}
The BorderedPage template border.htm:
<html>
<head>
<title>$title</title>
<link rel="stylesheet" type="text/css" href="style.css" title="Style"/>
</head>
<body>
<h2 class="title">$title</h2>
#parse($path)
</body>
</html>
Other pages insert their content into this template using the Velocity #parse directive, passing it their contents pages' path. The $path value is automatically added to the VelocityContext by the ClickServlet.
An example bordered Home page is provided below:
<page path="home.htm" classname="Home"/>
public class Home extends BorderedPage
{
public String title = "Home";
}
The Home page's content home.htm:
<b>Welcome</b> to Home page your starting point for the application.
When a request is made for the Home page (home.htm) Velocity will merge the border.htm page and home.htm page together returning:
<html>
<head>
<title>Home</title>
<link rel="stylesheet" type="text/css" href="style.css" title="Style"/>
</head>
<body>
<h2 class="title">Home</h2>
<b>Welcome</b> to Home page your application starting point.
</body>
</html>
Which may be rendered:
Home
Welcome to Home page your application starting point.
Note how the Home page class defines a title model value which is referenced in the border.htm template as $title. Each bordered page can define their own title which is rendered in this template.
Templating with JSP pages is also supported using the same pattern. Please see the Click Examples application for a demonstration.
Security
[edit | edit source]Pages provide an onSecurityCheck event handler which application pages can override to implement programmatic security model.
Please note you generally don't need to use this capability, and where possible you should use the declarative JEE security model.
Application Authentication
[edit | edit source]Applications can use the onSecurityCheck() method to implement their own security model. The example class below provides a base Secure page class which other pages can extend to ensure the user is logged in. In this example the login page creates a session when a user successfully authenticates. This Secure page then checks to make sure the user has a session, otherwise the request is redirected to the login page.
public class Secure extends Page
{
/**
* @see Page#onSecurityCheck()
*/
public boolean onSecurityCheck()
{
if (getContext().hasSession())
{
return true;
}
else
{
setRedirect(LoginPage.class);
return false;
}
}
}
Container Authentication
[edit | edit source]Alternatively you can also use the security services provided by the JEE Servlet Container. For instance to ensure users have been authenticated by the Serlvet Container you could use a Secure page of:
public class Secure extends Page
{
/**
* @see Page#onSecurityCheck()
*/
public boolean onSecurityCheck()
{
if (getContext().getRequest().getRemoteUser() != null)
{
return true;
}
else
{
setRedirect(LoginPage.class);
return false;
}
}
}
Container Access Control
[edit | edit source]The Servlet Container also provides facilities to enforce role based access control (authorization). The example below is a base page to ensure only users in the "admin" role can access the page, otherwise users are redirected to the login page. Application Admin pages would extend this secure page to provide their functionality.
public class AdminPage extends Page
{
/**
* @see Page#onSecurityCheck()
*/
public boolean onSecurityCheck()
{
if (getContext().getRequest().isUserInRole("admin"))
{
return true;
}
else
{
setRedirect(LoginPage.class);
return false;
}
}
}
Logging Out
[edit | edit source]To logout using the application or container based security models you would simply invalidate the session.
public class Logout extends Page
{
/**
* @see Page#onInit()
*/
public void onInit()
{
getContext().getSession().invalidate();
}
}
Stateful Pages
[edit | edit source]Click supports stateful pages where the state of the page is saved between the users requests. Stateful pages are useful in a number of scenarios including:
- Search page and edit page interactions, in this scenario you navigage from a stateful search page which may have filter criteria applied to an object edit page. Once object update has been completed on the edit page, the user is redirected to the search page and the stateful filter criteria still applied.
- Complex pages with multiple forms and or tables which need to maintain their state between interactions.
To make a page stateful you simply need to set the page [click-api/net/sf/click/Page.html#stateful stateful] property to be true and have the page implement the Serializable interface. For example:
package com.mycorp.page;
import java.io.Serializable;
import net.sf.click.Page;
public class SearchPage extends Page implements Serializable
{
public SearchPage()
{
setStateful(true);
..
}
}
Stateful page instances are stored in the user's HttpSession using the page's class name as the key. In the example above the page would be stored in the user's session using the class name: com.mycorp.page.SearchPage
Page Creation
[edit | edit source]With stateful pages they are only created once, after which they are retrieved from the session. However page event handlers are invoked for each request, including the onInit() method.
When you are creating stateful pages you typically place all your control creation code in the Pages constructor so it is invoked only once, and don't put this code in the onInit() method which will be invoked with each request.
If you have dynamic control creation code you would typically place this onInit() method, but you will need to take care that controls and or models are not already present in the page.
Page Execution
[edit | edit source]The default Click page execution model is thread safe as a new Page instance is created for each request and thread. With stateful pages a user will have a single page instance which is reused in multiple requests and threads. To ensure page execution is thread safe, a users page instances are synchronized so only one request thread can execute a page instance at any one time.
Page Destruction
[edit | edit source]After normal page instances have been executed, they are de-referenced and garbage collected by the JVM. However with stateful pages they are stored in the user's HttpSession so care needs to be take not store too many objects in stateful page instances which may cause memory and performance issues.
When pages have completed their execution, all the Page's controls onDestroy() methods are invoked, and then the Page's onDestroy() method is invoked. This is your opportunity to de-reference any large sets or graphs. For example the Table control by default de-references its rowList in its onDestroy() method.
Error Handling
[edit | edit source]If an Exception occurs processing a Page object or rendering a template the error is delegated to the registered handler. The default Click error handler is the ErrorPage, which is automatically configured as:
<page path="click/error.htm" classname="net.sf.click.util.ErrorPage"/>
To register an alternative error handler you must subclass ErrorPage and define your page using the path "click/error.htm". For example:
<page path="click/error.htm" classname="com.mycorp.page.ErrorPage"/>
When the ClickSevlet starts up it checks to see whether the error.htm template exists in the click web sub directory. If it cannot find the page the ClickServlet will automatically deploy one. You can tailor the click/error.htm template to suit your own tastes, and the ClickServlet will not overwrite it.
The default error template will display extensive debug information when the application is in development or debug mode. Example error page displays include:
- NullPointerException - in a page method
- ParseErrorException - in a page template
When the application is in production mode only a simple error message is displayed.
Page Not Found
[edit | edit source]If the ClickServlet cannot find a requested page in the click.xml config file it will use the registered not-found.htm page.
The Click not found page is automatically configured as:
<page path="click/not-found.htm" classname="net.sf.click.Page"/>
You can override the default configuration and specify your own class, but you cannot change the path.
When the ClickSevlet starts up it checks to see whether the not-found.htm template exists in the click web sub directory. If it cannot find the page the ClickServlet will automatically deploy one.
You can tailor the click/not-found.htm template to suit your own needs. This page template has access to the usual Click objects.
Message Properties
[edit | edit source]The Page class provides a messages property which is a MessagesMap of localized messages for the page. These messages are made available in the VelocityContext when the page is rendered under the key messages. So for example if you had a page title message you would access it in your page template as:
<h1> $messages.title </h1>
This messages map is loaded from the page classes property bundle. For example if you had a page class com.mycorp.page.CustomerList you could have an associated property file containing the page's localized messages:
/com/mycorp/page/CustomerList.properties
You can also defined a application global page messages properties file:
/click-page.properties
Messages defined in this file will be available to all pages through out your application. Note messages defined in your page class properties file will override any messages defined in the application global page properties file.
Page messages can also be used to override Control messages.