<security-constraint> <web-resource-collection> <web-resource-name>admin</web-resource-name> <url-pattern>/admin/*</url-pattern> </web-resource-collection> <auth-constraint> <role-name>admin</role-name> </auth-constraint> </security-constraint>The application user roles are defined in the web.xml file as security-role elements:
<security-role>
<role-name>admin</role-name>
</security-role>
The Servlet security model supports three different authentication method:
<login-config>
<auth-method>BASIC</auth-method>
<realm-name>Admin Realm</realm-name>
</login-config>
To use the FORM method you also need to specify the path to the login page and
the login error page:
<login-config> <auth-method>FORM</auth-method> <realm-name>Secure Realm</realm-name> <form-login-config> <form-login-page>/login.htm</form-login-page> <form-error-page>/login.htm?auth-error=true</form-error-page> </form-login-config> </login-config>In your Click login.htm page you need to include a special j_security_check form which includes the input fields j_username and j_password. For example:
#if ($request.getParameter("auth-error")) <div style="margin-bottom:1em;margin-top:1em;color:red;"> Invalid User Name or Password, please try again.<br/> Please ensure Caps Lock is off. </div> #end <form method="POST" action="j_security_check" name="form"> <table border="0" style="margin-left:0.25em;"> <tr> <td><label>User Name</label><font color="red">*</font></td> <td><input type="text" name="j_username" maxlength="20" style="width:150px;"/></td> <td> </td> </tr> <tr> <td><label>User Password</label><font color="red">*</font></td> <td><input type="password" name="j_password" maxlength="20" style="width:150px;"/></td> <td><input type="image" src="$context/images/login.png" title="Click to Login"/></td> </tr> </table> </form> <script type="text/javascript"> document.form.j_username.focus(); </script>When using FORM based authentication do NOT put application logic in a Click Login Page class, as the role of this page is to simply render the login form. If you attempt to put navigation logic in your Login Page class, the JEE Container may simply ignore it or throw errors. Putting this all together below is a web.xml snippet which features security constraints for pages under the admin path and the user path. This configuration uses the FORM method for authentication, and will also redirect unauthorized (403) requests to the /not-authorized.htm page.
<web-app> .. <error-page> <error-code>403</error-code> <location>/not-authorized.htm</location> </error-page> <security-constraint> <web-resource-collection> <web-resource-name>admin</web-resource-name> <url-pattern>/admin/*</url-pattern> </web-resource-collection> <auth-constraint> <role-name>admin</role-name> </auth-constraint> </security-constraint> <security-constraint> <web-resource-collection> <web-resource-name>user</web-resource-name> <url-pattern>/user/*</url-pattern> </web-resource-collection> <auth-constraint> <role-name>admin</role-name> <role-name>user</role-name> </auth-constraint> </security-constraint> <login-config> <auth-method>FORM</auth-method> <realm-name>Secure Zone</realm-name> <form-login-config> <form-login-page>/login.htm</form-login-page> <form-error-page>/login.htm?auth-error=true</form-error-page> </form-login-config> </login-config> <security-role> <role-name>admin</role-name> </security-role> <security-role> <role-name>user</role-name> </security-role> </web-app>
In this example application we use declarative role and path based security. All the pages in the admin package and directory require the "admin" role to be access, while all the pages in the user package and directory require the "user" role to be accessed.
public class BasePage extends Page implements ApplicationContextAware { /** The Spring application context. */ protected ApplicationContext applicationContext; /** The page Logger instance. */ protected Logger logger; /** * Return the Spring configured Customer service. * * @return the Spring configured Customer service */ public CustomerService getCustomerService() { return (CustomerService) getBean("customerService"); } /** * Return the Spring configured User service. * * @return the Spring configured User service */ public UserService getUserService() { return (UserService) getBean("userService"); } /** * Return the page Logger instance. * * @return the page Logger instance */ public Logger getLogger() { if (logger == null) { logger = Logger.getLogger(getClass()); } return logger; } /** * @see ApplicationContextAware#setApplicationContext(ApplicationContext) */ public void setApplicationContext(ApplicationContext applicationContext) { this.applicationContext = applicationContext; } /** * Return the configured Spring Bean for the given name. * * @param beanName the configured name of the Java Bean * @return the configured Spring Bean for the given name */ public Object getBean(String beanName) { return applicationContext.getBean(beanName); } }Applications typically use a border template and have a BorderPage which extends BasePage and defines the template. For example:
public class BorderPage extends BasePage { /** The root Menu item. */ public Menu rootMenu = new Menu(); /** * @see Page#getTemplate() */ public String getTemplate() { return "/border-template.htm"; } }Most application pages subclass BorderPage, except AJAX pages which have no need for a HTML border template and typically extend BasePage. The BorderPage class should not include common logic, other than that required for rendering the border template. Common page logic should be defined in the BasePage class. To prevent these base Page classes being auto mapped, and becoming directly acessible web pages, ensure that there are no page templates which could match their class name. For example the BorderPage class above will not be auto mapped to border-template.htm.
<click-app> <pages package="com.mycorp.dashboard.page"/> </click-app>To see how the page templates are mapped to Page classes set the application mode to debug and at startup the mappings will be listed out. An example Click startup listing is provided below:
[Click] [debug] automapped pages: [Click] [debug] /category-tree.htm -> com.mycorp.dashboard.page.CategoryTree [Click] [debug] /process-list.htm -> com.mycorp.dashboard.page.ProcessList [Click] [debug] /user-list.htm -> com.mycorp.dashboard.page.UserList
public class CustomerListPage extends Page { public ActionLink customerLink = new ActionLink(this, "onCustomerClick"); .. public boolean onCustomerClick() { Integer id = customerLink.getValueInteger(); Customer customer = getCustomerService().getCustomer(id); CustomerDetailPage customerDetailPage = (CustomerDetailPage) getContext().createPage(CustomerDetailPage.class); customerDetailPage.setCustomer(customer); setForward(customerDetailPage); return false; } }To redirect to another page using the Page class you can obtain the pages path from the Context. In the example below we are passing through the customer id as a request parameter to the target page.
public class CustomerListPage extends Page { public ActionLink customerLink = new ActionLink(this, "onCustomerClick"); .. public boolean onCustomerClick() { String id = customerLink.getValueInteger(); String path = getContext().getPagePath(CustomerDetailPage.class); setRedirect(path + "?id=" + id); return false; } }A quick way of redirecting to another page is to simply refer to the target class. The example below logs a user out, by invalidating their session, and then redirects them to the applications home page.
public boolean onLogoutClick() { getContext().getSession().invalidate(); setRedirect(HomePage.class); return false; }
#writeMenu($rootMenu)An advantage of using a macro to render your menu is that you can reuse the code across different applications, and to modify an applications menu you simply need to edit the WEB-INF/menu.xml file. A good place to define your macros is in the webroot /macro.vm file as it is automatically included by Click. Using macros you can create dynamic menu behaviour such as only rendering menu items a user is authorized to access with isUserInRoles().
#if ($menu.isUserInRoles()) .. #endYou can also use JavaScript to add dynamic behaviour such as drop down menus, for example see the Menu page in Click Examples.
public class BasePage extends Page { protected Logger logger; public Logger getLogger() { if (logger == null) { logger = Logger.getLogger(getClass()); } return logger; } }Using this pattern all your application bases should extend BasePage so they can use the getLogger() method.
public class CustomerListPage extends BasePage { public void onGet() { try { .. } catch (Exception e) { getLogger().error(e); } } }If you have some very heavy debug statement you should possibly use an isDebugEnabled switch so it is not invoked if debug is not required.
public class CustomerListPage extends BasePage { public void onGet() { if (getLogger().isDebugEnabled()) { String msg = .. getLogger().debug(msg); } .. } }Please note the Click logging facility is not designed for application use, and is for Click internal use only. When Click is running in production mode it will not produce any logging output.
<pages package="com.mycorp.page" automapping="true"/> <page path="click/error.htm" classname="ErrorPage"/> </pages>Generally application handle transactional errors using service layer code or via a servlet Filter and would not need to include error handling logic in an error page. Potential uses for a custom error page include custom logging. For example if an application requires unhandled errors to be logged to an application log (rather than System.out) then a custom ErrorPage could be configured. An example ErrorPage error logging page is provided below:
package com.mycorp.page.ErrorPage; .. public class ErrorPage extends net.sf.click.util.ErrorPage { public void onDestory() { Logger.getLogger(getClass()).error(getError()); } }
public class HomePage extends Page { private Form form = new Form("form"); public void onInit() { form.add(new DateField("date"); addControl(form); } public void getPageImports () { PageImports pageImports = super.getPageImports(); String contextPath = getContext().getRequest().getContextPath(); String cssInclude = contextPath + "/assets/css/home-page.css"; pageImports.addImport("<link type=\"text/javascript\" href=\"" + cssInclude + "\"/>"); String jsInclude = contextPath + "/assets/js/home-page.js"; pageImports.addImport("<script type=\"text/javascript\" src=\"" + jsInclude + "\"></script>"); // Set pageImports to initialized so that no other CSS and JavaScript files will be included. pageImports.setInitialized(true); } }Using the following border-template.htm:
<html> <head> <title>Click Examples</title> ${cssImports} </head> <body> ... ${jsImports} </body> </html>the rendered HTML will include one CSS and one JavaScript import:
<html> <head> <title>Click Examples</title> <link type="text/css" rel="stylesheet" href="/click-examples/assets/css/home-page.css" title="Style"/> </head> <body> ... <script type="text/javascript" src="/click-examples/assets/js/home-page.js"></script> </body> </html>A live demo is available here