$Author: bastafidli $
$Date: 2007/03/11 06:30:45 $
$Revision: 1.23 $
$RCSfile: tutorial_webapps.html,v $
Web applications have become increasingly popular in recent years mainly due to familiarity of users with web browsers and Internet web sites. More and more business applications are being delivered as web applications since once deployed on the Internet or intranet they become instantly available to large audience of users without forcing them to download or install any software.
Open Core makes developing web applications easier by providing set of classes that allow to easily make an application available on the Internet, handle the initialization, configuration, security, and tie together all the code developed so far. In addition, Open Core integrates Tiles template engine and provides several ready to use templates to speed up development of the web user interface.
We will decide what screens and web pages our user interface consists of. We can implement these easily by deriving them from one of the available templates without having to know much about HTML. We can customize the default look and feel by providing custom stylesheets that modify the behavior of the default stylesheets provided by Open Core. Once the decision about the screens and navigation is made, we will connect the user interface with the business logic by implementing the web tier. This can be easily done by deriving our own servlets from the base classes provided by Open Core. These will handle for us security related tasks, session management, user interface configuration and initialization, etc.
DatabaseContextListener
- class responsible for initialization of database layer
for web applications. Since web application doesn't have
any main method, there is a need for an entry point where
all database components the application consists of are
initialized in a dynamic manner. DatabaseContextListener
allows you to define in your standard web.xml file what
subsystems the application consists of without hardcoding
the dependencies in the code or in proprietary configuration
files.
WebSessionServlet
- base class for all servlets that might be concerned with
controlling access to the resources provided by the web
application. It intercepts requests and if the application
requires authentication it suspends the unauthenticated
requests and allows them to resume once the user was
authenticated by the system. If authentication is not
required, this class provides additional help with session
handling, coding of common logic, etc.
WebUIServlet
- base class for all servlets that display results of a
business logic processing using some web user interface. It
allows to configure the pages responsible for rendering of
the business logic results using standard web.xml or if
desired using application specific configuration file.
WebUIDispatchServlet
- servlet derived from
WebUIServlet
that simply redirects requests to a configured resource
while still enforcing authentication established by
WebSessionServlet.
WebModule,
WebModuleDefinitionManager,
WebModuleListener
and WebModuleTag
- classes allowing to divide the application user interface
into modules that can be constructed and operate
independently. WebModuleListener allows to define in the
standard web.xml file what modules the user interface
consists of without hardcoding the dependencies in the
code or in a proprietary configuration files.
WebModuleDefintionManager and WebModuleTab make easy to
generate visual representation of the available modules in
the application user interface.
The user interface of OpenChronicle consists of 10 pages, but it takes only three pages to create the entire skeleton of the application. These three pages will be used to browse all the chronicles and entries:
blogindex.jsp
blogviewer.jsp
blogentryviewer.jsp
As you may have noticed, these three pages work in two modes. For users that are logged in, the pages alllow to modify the data they display. For users that are not logged in, these pages just display the data for their reading and watching pleasure. Next four pages allow simple maintenance of chronicles and entries and are available only to logged in users:
newblog.jsp
newblogentry.jsp
confirmdeleteblog.jsp
confirmdeleteblogentry.jsp
Next two pages are related to authentication. Open Core provides all the necessary plumbing to ensure that user is authenticated when necessary and that the user interface knows about it. We just need to provide a "face" for this process.
login.jsp
logout.jsp
At last comes the page that handles errors. User can enter URL that may not correspond to any existing chronicle or entry. User can also click on a link to chronicle or entry, which was meanwhile deleted. This page improves the user experience in case of an error:
missingpageerror.jsp
So far this looks like a regular web application with couple of JSPs. What sets it apart is that when you look at the page source code you will notice that they are not concerned with the look and feel of the application or the fact that HTML pages require particular structure, blocks and elements that have only little to do with our application. This all thanks to the fact that the pages are using single common template implemented as another JSP page:
blog.jsp
You can read more about how the template engine works, how easy it is to create templates and pages using these templates and about the templates Open Core provides in separate document. Let's discuss some Open Core specifics to demonstrate the benefits it brings when developing web applications.
We have decided to provide user friendly layout for our
application with always visible header, footer and content
that fills out the space in between and can scroll if
necessary. To achieve this without Open Core we would have to
use some tricky CSS or JavaScript. In our case it was easy.
The layout blog.jsp just specifies that it wants
to reuse Open Core provided
template called basic
website that offers these capabilities.
<%-- Here we specify what main layout we use. This acts as inheritance mechanism. --%>
<tiles:insert page="/core/jsp/layout/basicwebsite.jsp" flush="true">
Now instead of coding the entire page from scrach, it is easy to just specify that we want to reuse the application layout:
<%-- This page is reusing global layout --%>
<tiles:insert page="/blog/jsp/layout/blog.jsp" flush="true">
and then enter the values for the title of the page and what should be its content, for example:
<tiles:put name="blogtitle" value="Delete chronicle"/>
<%-- Main content area --%>
<tiles:put name="blogcontent" direct="true">
<%-- Form to delete existing blogs --%>
<logic:equal name="loggedin" value="true">
<form method="post" action="">
<input type="hidden"
name="<%=WebUIServlet.FORM_NAME_REQUEST_PARAM%>"
value="FORM_DELETE_BLOG">
...
</form>
</logic:equal>
</tiles:put>
Many pages and the layout provide slightly different content based on the fact if user is logged in or not. It is easy to detect this scenario, since Open Core automatically sets flag loggedin to true, when user is logged in. You just need to declare it and use it in your page, for example:
<bean:define id="loggedin" name="loggedin" scope="request" type="java.lang.Boolean"/>
...
<logic:equal name="loggedin" value="false">
<a href="<%=contextpath%>/<%=BlogNavigator.LOGIN_WEB_PAGE%>">Login</a>
</logic:equal>
<%-- If user is logged in, he can modify the blog data --%>
<logic:equal name="loggedin" value="true">
<a href="<%=contextpath%>/<%=BlogNavigator.LOGOUT_WEB_PAGE%>">Logout</a>
</logic:equal>
You can find more details by examining the source code of the pages but with such time savers as these creating the entire user interface for our application is just a matter or few hours.
The web pages we have just designed and coded will display our application in the browser that can run on any computer connected to the server running the application over the Internet or an intranet. The server has to contain code, that can communicate with the browser and translate it's requests to format required by the business logic APIs we have implemented earlier. This code is part of the application web tier and is implemented using simple Java servlets.
The skeleton of the application enables user to browse the
chronicles and entries. All this functionality is concentrated
in a single class,
BlogBrowserServlet
.
This servlet is not concerned with how the data are created or
modified or how and if at all user logs in to the application.
This servlet is responsible for displaying the web pages to
user and therefore it is derived from the
WebUIServlet
servlet. The first thing the blog browser servlet needs to do
is to identify the JSP pages that will be used to display the
data since these are fully configurable. In our application it
is done in the init method using functionality
provided by the parent class.
public static final String BLOGBROWSER_BLOG_INDEX_PAGE = "blogbrowser.blog.index.page";
public void init(
ServletConfig scConfig
) throws ServletException
{
super.init(scConfig);
// Load UI pages for this blog
// The main entry point to the blog functionality
cacheUIPath(scConfig, BLOGBROWSER_BLOG_INDEX_PAGE,
"Path to main index page is not set in property "
+ BLOGBROWSER_BLOG_INDEX_PAGE);
...
}
Once the pages are ready to be used, the next step is to
identify when to display which page. Each request from the
browser to display read only data is received in method
doGet. The implementation examines the URL for
the requested static HTML page and maps it to either list of
chronicles, single chronicle or a single entry from the
chronicle. If the URL doesn't make sense an error message page
we have created earlier will be displayed to the user.
protected void doGet(
HttpServletRequest hsrqRequest,
HttpServletResponse hsrpResponse
) throws ServletException,
IOException
{
if (WebUtils.isIndexPage(hsrqRequest))
{
if (WebUtils.isMainIndexPage(hsrqRequest))
{
// Create page displaying all existing blogs
createMainIndexPage(hsrqRequest, hsrpResponse);
}
else
{
// Create page displaying one blog
createIndexPage(hsrqRequest, hsrpResponse);
}
}
else
{
if (WebUtils.isStaticWebPage(hsrqRequest))
{
// Create page displaying one entry of a blogs
createBlogEntryPage(hsrqRequest, hsrpResponse);
}
else
{
hsrpResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
}
The last step is to display the requested page:
protected void createIndexPage(
HttpServletRequest hsrqRequest,
HttpServletResponse hsrpResponse
) throws IOException,
ServletException
{
s_logger.entering(this.getClass().getName(), "createIndexPage");
try
{
Object objBlogIdentification;
Object[] arObjects;
BlogNavigator navigator;
navigator = getNavigator(hsrqRequest);
objBlogIdentification = navigator.getBlogIdentification(hsrqRequest);
arObjects = getController().getWithEntries((String)objBlogIdentification);
if ((arObjects != null) && (arObjects[0] != null))
{
hsrqRequest.setAttribute("blog", arObjects[0]);
// It is ok to do not have any entries
if (arObjects[1] != null)
{
hsrqRequest.setAttribute("blogentries", arObjects[1]);
}
hsrqRequest.setAttribute("blognavigator", navigator);
displayUI(BLOGBROWSER_BLOG_VIEWER_PAGE, hsrqRequest, hsrpResponse);
}
else
{
// We have found either the blog or the entry so tell the user
// about it
hsrpResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
catch (Exception eExc)
{
s_logger.log(Level.WARNING, "An error has occured while retrieving blog.",
eExc);
messageBoxPage(hsrqRequest, hsrpResponse, "Error", eExc.getMessage(),
getNavigator(hsrqRequest).getRootURL(),
eExc.getCause());
}
finally
{
s_logger.exiting(this.getClass().getName(), "createIndexPage");
}
}
Notice how the controller is used to invoke the business logic
and retrieve all the data web page needs at once. The
displayUI method is used to display the page
(cached earlier in the init method) that will
render the retrieved data.
When logged in user browses through the chronicles and entries,
the generated web pages provide him an opportunity to modify
data on the server using forms, edit fields and other controls.
Once user submits the data modifications to the server, the
server needs to accept such requests and send them to the
business logic for processing. All this functionality is
provided by a single class,
BlogEditServlet
,
derived from
BlogBrowserServlet
.
Thanks to the inheritance this servlet has now the opportunity
to do both, browse the data and modify them if the user allowed.
The developer has now option to create either a read only
version of the application using the BlogBrowserServlet or
application that allows modifying the data using
BlogEditServlet.
The edit servlet caches pages that allow the data modification
(e.g. creation or deletion) in the init method the
same way as described earlier. Each request to modify the data
is received and processed by the method doPost.
Implementation of this method recognizes what action has the
user requested and forwards it for processing.
protected void doPost(
HttpServletRequest hsrqRequest,
HttpServletResponse hsrpResponse
) throws ServletException, IOException
{
switch(getFormToProcess(hsrqRequest))
{
...
case FORM_NEW_BLOG_ID :
{
processNewBlogForm(hsrqRequest, hsrpResponse);
break;
}
case FORM_CREATE_BLOG_ID :
{
processCreateBlogForm(hsrqRequest, hsrpResponse);
break;
}
...
default :
{
super.doPost(hsrqRequest, hsrpResponse);
break;
}
}
}
Each request for data modification is accompanied by a form name defined in the web page we have previously constructed. The web page can for example contain form
<%-- Form with navigation to create new blogs --%>
<form method="post" action="<bean:write name="blognavigator"
property="postURL"/>">
<input type="hidden"
name="<%=WebUIServlet.FORM_NAME_REQUEST_PARAM%>"
value="FORM_EDIT_BLOG">
<input type="submit" name="create" value="Start new chronicle...">
</form>
and the servlet uses the FORM_NAME variable value
to detect the user's action. The detection is performed in
getFormToProcess method overridden from the Open
Core provided servlet base class.
protected int getFormToProcess(
HttpServletRequest hsrqRequest
)
{
String strFormName;
int iReturn = FORM_UNKNOWN_ID;
strFormName = hsrqRequest.getParameter(FORM_NAME_REQUEST_PARAM);
if (strFormName == null)
{
iReturn = super.getFormToProcess(hsrqRequest);
}
...
else if (strFormName.equals(FORM_CREATE_BLOG_NAME))
{
// Create new blog from already supplied details
iReturn = FORM_CREATE_BLOG_ID;
}
...
else if (strFormName.equals(FORM_DELETE_BLOGENTRY_NAME))
{
// Delete current blog
iReturn = FORM_DELETE_BLOGENTRY_ID;
}
else
{
iReturn = super.getFormToProcess(hsrqRequest);
}
return iReturn;
}
There might be two kinds of requests for data modifications. The first kind is requests to display page that enables some data modification but doesn't actually perform one itself. These pages may not normally accessible by browsing the existing data. These kinds of requests are handled the same way as the other read only requests by retrieving the data and rendering it on the page.
protected void processNewBlogForm(
HttpServletRequest hsrqRequest,
HttpServletResponse hsrpResponse
) throws IOException,
ServletException
{
s_logger.entering(this.getClass().getName(), "processNewBlogForm");
try
{
Blog blog;
// Create template object which will be used to will the form with
// default values
blog = (Blog)getController().get(DataObject.NEW_ID);
hsrqRequest.setAttribute("blog", blog);
hsrqRequest.setAttribute("blognavigator", getNavigator(hsrqRequest));
displayUI(BLOGEDIT_NEW_BLOG_PAGE, hsrqRequest, hsrpResponse);
}
catch (Exception eExc)
{
s_logger.log(Level.WARNING,
"An error has occured while retrieving information about" +
" new blog.", eExc);
messageBoxPage(hsrqRequest, hsrpResponse, "Error", eExc.getMessage(),
getNavigator(hsrqRequest).getRootURL(),
eExc.getCause());
}
finally
{
s_logger.exiting(this.getClass().getName(), "processNewBlogForm");
}
}
Notice above that even pages allowing user to create the data use the controller to retrieve empty template object, which is then used to populate the default values to the form when the page is rendered.
The second kind of requests is the one actually asking the server to modify some data and is usually accompanied by the changed data. Processing of these requests consists of three steps. First step retrieves the changed data from the request.
protected Blog parseBlogFromRequest(
HttpServletRequest hsrqRequest
)
{
String strTemp;
int iBlogId;
int iDomainId = DataObject.NEW_ID;
String strFolder;
String strCaption;
String strComments;
Timestamp creationDate;
Timestamp modificationDate;
strTemp = hsrqRequest.getParameter("BLOG_ID");
if (strTemp == null)
{
throw new IllegalArgumentException("Blog id is not specified.");
}
iBlogId = Integer.parseInt(strTemp);
iDomainId = CallContext.getInstance().getCurrentDomainId();
strFolder = hsrqRequest.getParameter("BLOG_FOLDER");
if (strFolder == null)
{
strFolder = "";
}
...
// User can never change modification date, therefore we require to store
// it as the millisecond value since that is independent from the display format
strTemp = hsrqRequest.getParameter("BLOG_MODIFICATION_DATE");
if (strTemp == null)
{
modificationDate = null;
}
else
{
modificationDate = DateUtils.parseTimestamp(strTemp);
}
return new Blog(iBlogId, iDomainId, strFolder, strCaption, strComments,
creationDate, modificationDate);
}
In the second step the servlet uses the controller to send the retrieve data to the business logic for processing. The last step involves handling of any errors that may have occurred.
protected void processCreateBlogForm(
HttpServletRequest hsrqRequest,
HttpServletResponse hsrpResponse
) throws IOException,
ServletException
{
s_logger.entering(this.getClass().getName(), "processCreateBlogForm");
UserTransaction transaction = null;
Blog blog = null;
try
{
transaction = DatabaseTransactionFactoryImpl.getInstance().requestTransaction();
blog = parseBlogFromRequest(hsrqRequest);
Blog blogUpdated = null;
// Since one request corresponds to user transaction and we know we are
// going to modify data, start database transaction to group any
// databast modification and queries we may need to execute to satisfy
// this action.
transaction.begin();
blogUpdated = (Blog)getController().create(blog);
transaction.commit();
// Redirect user to the page displaying newly created blog
hsrpResponse.sendRedirect(getNavigator(hsrqRequest).getURL(blogUpdated));
}
catch (OSSInvalidDataException ideExc)
{
TransactionUtils.rollback(transaction);
CallContext.getInstance().getMessages().addMessages(
ideExc.getErrorMessages());
// Return the submitted value with the modified entries
hsrqRequest.setAttribute("blog", blog);
hsrqRequest.setAttribute("blognavigator", getNavigator(hsrqRequest));
displayUI(BLOGEDIT_NEW_BLOG_PAGE, hsrqRequest, hsrpResponse);
}
catch (Throwable thr)
{
TransactionUtils.rollback(transaction);
s_logger.log(Level.WARNING, "An error has occured while creating blog.", thr);
messageBoxPage(hsrqRequest, hsrpResponse, "Error", thr.getMessage(),
getNavigator(hsrqRequest).getRootURL(),
thr.getCause());
}
finally
{
s_logger.exiting(this.getClass().getName(), "processCreateBlogForm");
}
}
Notice how errors are handled in two different ways. Errors
involving invalid data are displayed back to the user using the
same data and on the same page from which the request was sent.
This allows the user to correct the data and resubmit the
request. CallContext object allows convenient way
how to send such error messages to the user interface.
Fatal errors, which are for example caused by loss of database connectivity are handled using generic message box page provided by Open Core.
Each method that modifies data should also handle transactions
to ensure consistency of the data. Either all data
modifications will be performed at the moment when the
transaction is committed or all will be cancelled when the
transaction is rolled back. This step is necessary since if the
application is deployed in non J2EE environment (e.g. as POJO)
there is no container to handle the transactions for us. The
nice thing is that Open Core provides the
UserTransaction object, which will work in all
environments and therefore the above code will work for both,
POJO or J2EE.
Application is now almost complete. It allows creating, modifying or deleting data and browsing the existing data. What is still missing is to ensure that only the authorized users can create or change the data.
Next:
Securing the web application
Previous:
Developing the business logic