HierarchicalMap Tutorial - Advanced Level
HierarchicalMap is an interface. So anyone can implement it. This tutorial is based on BasicHierarchicalmap, a reference implementation of HierarchicalMap.
Following subjects are covered in this tutorial:
(Javascript must be enabled to allow syntax highlighting for source code snippets in this tutorial)
Advanced Level
14. Writing ApplicationNow, let us mix everything up. In this chapter, we will create a web application based on Tomcat (could be any other Servlet Container).
We assume that there is model like below, presented at "Accessing Data Base" tutorial, implemented on your data base server.
First, create a web project using your prefered IDE.
Using Eclipse:
or Using Netbeans:
Then, start creating an html form as shown:
ISBN:
Title:
Price:
To do so, create a file like below, name it "BookForm.html" and place it under WebContent/template on your project.
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xmap="http://www.dhmp.org/xsd/StylesheetScript"> <head> <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> <title>BookForm</title> </head> <body> <h1>Book</h1> <form action="BookForm"> <div> <p>ISBN:<input type="text" name="book/isbn" /></p> <p>Title:<input type="text" name="book/title" /></p> <p>Price:<input type="text" name="book/price" /></p> <input type="hidden" name="book/id"/> <input type="submit" name="buttom" value="Ok" /> <input type="submit" name="buttom" value="Cancel" /> </div> </form> </body> </html>
Remember to use same charset all over your project. On the example above, we are using UTF-8, but it could be any other charset like ISO-8559-1.
Now, create a Servlet named BookForm and add the following code:
private ScriptCollection scripts; private Script script; /** * @see HttpServlet#HttpServlet() */ public BookForm() { super(); } public void init() { String url = this.getServletContext() .getRealPath("/template"); scripts = new ScriptCollection(new File(url)); script = scripts.get("BookForm.html", Charset.forName("UTF-8")); } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doPost(request, response); } /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { script.eval(response.getWriter(), null); }
Note that ScriptCollection and Script are both from org.dhmp.util.stylesheet.
The content of WebContent/WEB-INF/web.xml should be like following:
<?xml version="1.0" encoding="UTF-8"?> <web-app id="WebApp_ID" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"> <display-name>Application</display-name> <servlet> <servlet-name>BookForm</servlet-name> <servlet-class>BookForm</servlet-class> </servlet> <servlet-mapping> <servlet-name>BookForm</servlet-name> <url-pattern>/BookForm</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>index.html</welcome-file> </welcome-file-list> </web-app>
Finally, make sure that dhmp.jar (containing HierarchicalMap classes) and xercesImpl.jar are visible to Tomcat.
Place them under WebContent/WEB-INF/lib, or $CATALINA_HOME/lib,
or some other directory and reference the latter directory using shared.loader
entry on $CATALINA_HOME/conf/catalina.properties.
(e.g. shared.loader=${catalina.home}/lib/ext/*.jar)
Launch Tomcat under debug option, or standalone after deploying the war file, and check the result, calling http://localhost:8080/Application/BookForm.
Getting Parameter
Now, add a method, which is shown next, to convert parametetr to a HierarchicalMap and check the value returned when the BookForm is submitted.
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // convert parameter to HierarchicalMap HierarchicalMap param = parseParameter(request); script.eval(response.getWriter(), null); } public HierarchicalMap parseParameter(HttpServletRequest request) { HierarchicalMap param = new BasicHierarchicalMap(); for (Map.Entry e: request.getParameterMap().entrySet()) { String[] parameter = (String[]) e.getValue(); for (int i = 0; i < parameter.length; i++) { param.add(e.getKey(), parameter[i]); } } return param; }
Validating Parameter
Before proceeding, the application must check the input parameter provided by user. Put in mind that the form's parameter will be used to insert records into database table. Therefore, we will define all the constraints using xml schema as follows: (review the Runtime Schema Validation tutorial)
<?xml version="1.0" encoding="UTF-8"?> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:dhmp= "http://www.dhmp.org/xsd/SchemaErrorMessage.xsd" elementFormDefault="qualified"> <xs:element name="book"> <xs:complexType> <xs:sequence> <xs:element name="isbn" type="xs:string"/> <xs:element name="title" type="xs:string"/> <xs:element name="price"> <xs:annotation> <xs:appinfo> <dhmp:message text="price must be decimal with 2 fraction digits and can not be 0."/> </xs:appinfo> </xs:annotation> <xs:simpleType> <xs:restriction base="xs:decimal"> <xs:minExclusive value="0"/> <xs:fractionDigits value="2"/> </xs:restriction> </xs:simpleType> </xs:element> </xs:sequence> </xs:complexType> </xs:element> </xs:schema>
Create the insert_book.xsd with above content and place it under WebContent/xsd. Note that the elements are presented in exactly same order and type as defined in stored procedure's parameters.
Now, change the servlet code as follows. Run it and see the returning HierarchicalMap value (sqlparam).
First, add two more static attrributes. One is SchemaCollection schemas, and the other is Schema schema. Then, alter init() method as code shown next:
script = scripts.get("BookForm.html", Charset.forName("UTF-8")); schemas = new SchemaCollection(); try { insert_book_schema = schemas.get(this.getServletContext() .getResource("/xsd/insert_book.xsd")); // instruct schema to add error message during // transformation insert_book_schema.setAddErrorMessage(true); } catch (SchemaException e) { Logger.getLogger(BookForm.class.getName()) .log(Level.SEVERE, null,e); } catch (MalformedURLException e) { Logger.getLogger(BookForm.class.getName()) .log(Level.SEVERE, null,e); }
Also change the doPost() method as follows:
// convert parameter to HierarchicalMap HierarchicalMap param = parseParameter(request); if ("Ok".equals(param.get("buttom"))) { // transform input parameter to correct sequence // required by insert_book stored procedure HierarchicalMap sqlparam = insert_book_schema .transform(param); if(sqlparam.get("_error") != null) { // validation error } } script.eval(response.getWriter(), null);
Try several combination of input parameter for the form, submit them and see the content of sqlparam HierarchicalMap. Note the difference when form content attend all the requirement defined in schema document, and when it does not fullfill the constraints.
The rule behind this functionality is: when an element does not meet the constraint, an error node "_error" with the error message is appended at the node that contains the element. And "_focus" leaf with key associated to the first element, which does not satisfy the constraint, is added to the root. Ok, it is bit confusing, but we could not find an easier solution at the moment. We are looking forward to receive your suggestion to make this simpler.
Presenting Error Message
Next step is to present the validation error message on the form.
Alter the BookForm.html adding some substitution elements. Basically, we are adding a place to present error message and also defining a value for elements.
One more thing, there is also a tip to handle focus setting. Unfortunately, html's id can not have "/", so it should be a simple name and must be identical to the last portion of element's name.
<body onload= "document.forms.bookform.<xmap:substitution name='_focus' default='isbn' />.focus()" > <h1>Book</h1> <form action="BookForm" id="bookform"> <div> <span style="color:#ff0000;"><xmap:substitution name="book/_error/isbn" /></span> <p>ISBN:<input type="text" name="book/isbn" id="isbn" value="<xmap:substitution name='book/isbn' />"/></p> <span style="color:#ff0000;"><xmap:substitution name="book/_error/title" /></span> <p>Title:<input type="text" name="book/title" id="title" value="<xmap:substitution name='book/title' />"/></p> <span style="color:#ff0000;"><xmap:substitution name="book/_error/price" /></span> <p>Price:<input type="text" name="book/price" id="price" value="<xmap:substitution name='book/price' />"/></p>
To present the error message after validation, modify the code on doPost() method as below:
HierarchicalMap sqlparam = insert_book_schema .transform(param); if(sqlparam.get("_focus") != null) { // validation error script.eval(response.getWriter(), sqlparam); return; }
Inserting Book
So far, the Servlet is producing a ordinal html file. We will improve its functionality to insert new record on database when fillout the form and submit it.
Make sure that stored procedure insert_book, of Accessing Data Base tutorial has created on your database. Then, configure your Tomcat to recognize the database. In this example, we are using c3p0 as connection pool and Postgres as database Place the c3p0's jar file and Postgres's jdbc jar file on the same directory where xercesImpl.jar is placed. Then, create a WebContent/META-INF/context.xml with following content:
<?xml version="1.0" encoding="UTF-8"?> <Context path="/iMob"> <!-- Default set of monitored resources --> <WatchedResource>WEB-INF/web.xml</WatchedResource> <Resource acquireIncrement="1" auth="Container" description="DB Connection" driverClass="org.postgresql.Driver" factory="org.apache.naming.factory.BeanFactory" jdbcUrl="jdbc:postgresql://localhost:5432/postgres" maxPoolSize="5" minPoolSize="3" name="jdbc/postgres" password="dhmp.org" type="com.mchange.v2.c3p0.ComboPooledDataSource" user="postgres"/> </Context>
Then, alter the WebContent/WEB-INF/web.xml adding resource-ref as follow:
<display-name>Application</display-name> <resource-ref> <description>DataSource Reference</description> <res-ref-name>jdbc/postgres</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> </resource-ref> <servlet> <servlet-name>BookForm</servlet-name> <servlet-class>BookForm</servlet-class> </servlet>
At last, change the doPost() method as described below:
if (sqlparam.get("_focus") != null) { // validation error script.eval(response.getWriter(), sqlparam); return; } try { // get the database connection Context ctx; ctx = new InitialContext(); DataSource ds = (DataSource) ctx .lookup("java:comp/env/jdbc/postgres"); Connection con = ds.getConnection(); MapSQLStatement msql = new MapSQLStatement(con); msql.setStatement("{ call insert_book(?, ?, ?) }"); msql.execute(sqlparam); msql.close(); } catch (NamingException e) { Logger.getLogger(BookForm.class.getName()).log( Level.SEVERE, null, e); } catch (SQLException e) { Logger.getLogger(BookForm.class.getName()).log( Level.SEVERE, null, e); }
Listing Books
For this part, we will use the list_book stored procedure mentioned at "Acessing DataBase" tutorial.
Prepare an html template as below and put it at WebContent/template/BookListFrom.html.
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xmap="http://www.dhmp.org/xsd/StylesheetScript"> <head> <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> <title>BookForm</title> </head> <body> <h1>Book List</h1> <div> <table border="1"> <tr> <th>ISBN</th> <th>Title</th> <th>Price</th> </tr> <xmap:foreach in="book"> <tr> <td><xmap:substitution name="isbn">123-1234567890</xmap:substitution></td> <td><xmap:substitution name="title">Title</xmap:substitution></td> <td><xmap:substitution name="price">99.99</xmap:substitution></td> </tr> </xmap:foreach> </table> </div> </body> </html>
Create a new Servlet named BookListForm, with following methods:
public void init() { String url = this.getServletContext() .getRealPath("/template"); scripts = new ScriptCollection(new File(url)); script = scripts.get("BookListForm.html", Charset.forName("UTF-8")); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doPost(request, response); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { // get the database connection Context ctx; ctx = new InitialContext(); DataSource ds = (DataSource) ctx .lookup("java:comp/env/jdbc/postgres"); Connection con = ds.getConnection(); MapSQLStatement msql = new MapSQLStatement(con); msql.setStatement("{ ? = call list_book() }"); HierarchicalMap param = new BasicHierarchicalMap(); param.put("book", new MapSQLStatement. OutParameter(java.sql.Types.OTHER)); //Disable auto commit to preserve cursor con.setAutoCommit(false); HierarchicalMap result = msql.execute(param); msql.close(); script.eval(response.getWriter(), result); } catch (NamingException e) { Logger.getLogger(BookForm.class.getName()) .log(Level.SEVERE, null, e); } catch (SQLException e) { Logger.getLogger(BookForm.class.getName()) .log(Level.SEVERE, null, e); } }
Now, call http://localhost:8080/Application/BookListForm and see the result.
Everything is working fine, though we want to remove all the stored procedure's specific treatment out of our java code. For that, we will introduce a new class: SQLSchema.
Prepare a new XML Schema as follows:
<?xml version="1.0" encoding="UTF-8"?> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:dhmp="http://www.dhmp.org/xsd/SchemaErrorMessage.xsd" xmlns:ext="http://www.dhmp.org/xsd/SchemaExtension.xsd" elementFormDefault="qualified"> <xs:annotation> <xs:documentation>List all book records</xs:documentation> <xs:appinfo> <ext:sql statement="{ ? = call list_book() }" /> </xs:appinfo> </xs:annotation> <xs:element name="book"> <xs:annotation> <xs:appinfo> <ext:sql direction="out" typevalue="1111" /> </xs:appinfo> </xs:annotation> </xs:element> </xs:schema>
Note that there is a new element "ext:sql". This is an extension for defining SQL specific information.
Placed at the top level, right under schema element, we can define the SQL statement, which can be retrieved using SQLSchema class.
Placed at the element, we can define the direction of the parameter, that can be "out", "inout" or "in" ("in" is default and can be omitted). And also the SQL type of the "out" parameter. The type must be defined using the integer value as defined in java.sql.Types (1111 of above XML schema corresponds to java.sql.Types.OTHER).
The BookListForm servlet, can be changed as below:
public class BookListForm extends HttpServlet { private ScriptCollection scripts; private Script script; private SchemaCollection schemas; private SQLSchema list_book_schema; public void init() { String url = this.getServletContext() .getRealPath("/template"); scripts = new ScriptCollection(new File(url)); script = scripts.get("BookListForm.html", Charset.forName("UTF-8")); schemas = new SchemaCollection(); try { list_book_schema = new SQLSchema( schemas.get(this.getServletContext() .getResource("/xsd/list_book.xsd"))); } catch (SchemaException e) { Logger.getLogger(BookForm.class.getName()) .log(Level.SEVERE, null, e); } catch (MalformedURLException e) { Logger.getLogger(BookForm.class.getName()) .log(Level.SEVERE, null, e); } } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doPost(request, response); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { // get the database connection Context ctx; ctx = new InitialContext(); DataSource ds = (DataSource) ctx .lookup("java:comp/env/jdbc/postgres"); Connection con = ds.getConnection(); MapSQLStatement msql = new MapSQLStatement(con); msql.setStatement(list_book_schema.getStatement()); HierarchicalMap param = list_book_schema .transform(null); //Disable auto commit to preserve cursor con.setAutoCommit(false); HierarchicalMap result = msql.execute(param); msql.close(); script.eval(response.getWriter(), result); } catch (NamingException e) { Logger.getLogger(BookForm.class.getName()) .log(Level.SEVERE, null, e); } catch (SQLException e) { Logger.getLogger(BookForm.class.getName()) .log(Level.SEVERE, null, e); } } }
Well, see next the brief explanation of what is happening with the code above.
In the init() method, we are loading the XML schema document into list_book_schema. Note that this time, we are instantiating SQLSchema passing the retrieved Schema to its constructor. Also observe that we are not calling setAddErrorMessage method. This is because the stored procedure does not have input parameters, thus we are no longer interested in validating the input parameter.
Look at doPost() method. Now, we are retrieving SQL Statement defined in XML schema and passing it to MapSQLStatement, and just calling schema's transform() method to create a HierarchicalMap containing the output parameter as specified by schema document.
Summary
Most of things presented here are ordinal servlet programming under Tomcat.
In essence, following steps should be taken for writing application using HierarchicalMap: