In this article, we shall continue from "A Java Servlet E-shop Case Study".
I shall assume that you are familiar with:
- MySQL (read "How to install MySQL and Get Start with Java Database Programming").
- Tomcat (read "How to install Tomcat and Get Started with Java Servlet Programming").
- JDBC (read "JDBC Basic").
- You have completed the "Java Servlet E-Shop Case Study".
Touching Up our E-Bookshop
Before we proceed, let us first touch up the e-bookshop that we have written earlier. We shall create a new webapp called "yaebookshop
" (yet another e-bookshop).
I suggest that you use an IDE (with a graphic debugger), such as NetBeans or Eclipse, to develop our webapp, so as to improve your productivity and efficiency. Read:
- Eclipse: "Developing and Deploying Web Applications in Eclipse for Java EE".
- NetBeans: "Developing and Deploying Web Application in NetBeans".
- VS Code: [TODO]
Create a New Webapp "yaebookshop
"
Without IDE (With Text Editor and JDK)
Create the following webapp directory structure for "yaebookshop
" under Tomcat's "webapps
" directory:
- webapps\yaebookshop: The context root for the webapp "
yaebookshop
". Contains resources visible to the web users, such as HTML, CSS, images and scripts. - webapps\yaebookshop\WEB-INF: Hidden directory for protected resources of this webapp. Contains the web application deployment descriptor "
web.xml
", and,- webapps\yaebookshop\WEB-INF\src: Keeps the Java Source file.
- webapps\yaebookshop\WEB-INF\classes: Keeps the Java classes.
- webapps\yaebookshop\WEB-INF\lib: Keep the external library's JAR-files, e.g., the MySQL JDBC Driver.
- webapps\yaebookshop\META-INF: Hidden directory for server-related resource specific to the server (such as Tomcat, Glassfish), e.g., configuration file "
context.xml
". In contrast, "WEB-INF
" is for resources independent of the web server.
Eclipse-JavaEE (v2021)
Reference: "Developing and Deploying Web Applications in Eclipse for Java EE".
Create a webapp called "yaebookshop
":
- Launch Eclipse-JavaEE, choose a workspace directory (any directory of your choice but NOT the Tomcat's "webapps" directory).
- From "File" menu ⇒ New ⇒ Others ⇒ Web ⇒ Dynamic Web Project ⇒ Next ⇒ Under the Project Name: enter "yaebookshop" ⇒ Next ⇒ Next ⇒ Observe that "Context Root" is "yaebookshop", "Content directory" is "WebContent" ⇒ Finish.
VS Code (2024)
- Create the webapp directory, as above.
- Under "
WEB-INF
", create sub-directory "src
", "classes
" and "lib
". - In "
WEB-INF
", create a file ".vscode\settings.json
" to specifysource
,classes
, andlib
paths.{ "java.project.sourcePaths": ["src"], "java.project.outputPath": "classes", "java.project.referencedLibraries": ["lib/**/*.jar"] }
- To remove compiler option
--enable-preview
(which is not supported in Tomcat's Java runtime): In "WEB-INF
", create a file ".settings/org.eclipse.jdt.core.prefs
" with the following line:org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
Read https://github.com/redhat-developer/vscode-java/wiki/Settings-Global-Preferences. - Copy "
$TOMCAT_HOME\lib\servlet-api.jar
" into "lib
" - needed to compile servlet in this workspace. - Launch VS Code. Open folder "
WEB-INF
". Build project.
NetBeans-JavaEE (v7)
Create a web application called "yaebookshop
":
- From "File" menu ⇒ choose "New Project".
- In "Choose Project" ⇒ Under "Categories", choose "Java Web" ⇒ Under "Projects", choose "Web Application" ⇒ "Next".
- In "Name and Location" ⇒ In "Project Name", enter "
yaebookshop
" ⇒ In "Project Location", select a suitable directory to save your works ⇒ In Project Folder, use the default ⇒ Check "Set as Main Project" ⇒ Next. - In "Server and settings" ⇒ In "Server", select your Tomcat server, or "add" a new Tomcat server ⇒ In "Java EE Version", choose the latest such as Java EE 6 Web ⇒ In "Context Path", use the default "
/yaebookshop
" ⇒ Next. - In "Frameworks" ⇒ Select none for pure servlet/JSP application ⇒ Finish.
Setup the Database
We shall use the same database as the basic case study.
Database: ebookshop Table: books +-------+----------------------------+---------------+---------+-------+ | id | title | author | price | qty | | (INT) | (VARCHAR(50)) | (VARCHAR(50)) | (FLOAT) | (INT) | +-------+----------------------------+---------------+---------+-------+ | 1001 | Java for dummies | Tan Ah Teck | 11.11 | 11 | | 1002 | More Java for dummies | Tan Ah Teck | 22.22 | 22 | | 1003 | More Java for more dummies | Mohammad Ali | 33.33 | 33 | | 1004 | A Cup of Java | Kumar | 44.44 | 44 | | 1005 | A Teaspoon of Java | Kevin Jones | 55.55 | 55 | +-------+----------------------------+---------------+---------+-------+ Database: ebookshop Table: order_records +-------+-------------+---------------+---------------+------------+ | id | qty_ordered | cust_name | cust_email | cust_phone | | (INT) | (INT) | (VARCHAR(30)) | (VARCHAR(30)) | CHAR(8) | +-------+-------------+---------------+---------------+------------+
You can create the database by running the following SQL script:
create database if not exists ebookshop; use ebookshop; drop table if exists books; create table books ( id int, title varchar(50), author varchar(50), price float, qty int, primary key (id)); insert into books values (1001, 'Java for dummies', 'Tan Ah Teck', 11.11, 11); insert into books values (1002, 'More Java for dummies', 'Tan Ah Teck', 22.22, 22); insert into books values (1003, 'More Java for more dummies', 'Mohammad Ali', 33.33, 33); insert into books values (1004, 'A Cup of Java', 'Kumar', 44.44, 44); insert into books values (1005, 'A Teaspoon of Java', 'Kevin Jones', 55.55, 55); drop table if exists order_records; create table order_records ( id int, qty_ordered int, cust_name varchar(30), cust_email varchar(30), cust_phone char(8)); select * from books;
Sequence of Events
The sequence of events is as follows:
- A client can start the webapp by issuing
URL http://hostname:port/yaebookshop/start
, which is mapped to "EntryServlet.class
". The servlet outputs an HTML form of a search menu. The form is to be submitted to URL "
/search
", with request parametersauthor=name
andsearch=term
. - The URL "
/search
" maps to "QueryServlet.class
", which retrieves the request parametersauthor=name
andsearch=term
, queries the database, and outputs the list of books that meets the query criteria in an HTML form. Each book is identified by a checkbox (withname=value
pair ofid=xxx
) and a quantity text field (with name=value pair ofidxxx=qty
, where xxx is the book'sid
). The form is to be submitted to URL "/order
". - The URL "
" maps to "/order
OrderServlet.class
", which retrieves the book's id and quantity ordered, and update the tablebooks
, by reducing the quantity available. It also create a transaction record in a tableorder_records
.
Entry Page - "EntryServlet.java
" (with URL "/start
" or "/entry
")
The entry servlet (called "EntryServlet.java
" with URL "/start
" or "/entry
") prints an HTML form with a pull-down menu and a text field for users to issue query. It populates the pull-down menu with all the available authors, by querying the database. It also provides a text field for users to enter a search term to search for the desired title or author with pattern matching. Take note that the entry point of this webapp is a servlet with URL http://hostname:port/yaebookshop/start
, instead of an HTML page as in the previous exercises.
For proper deployment, Java classes shall be kept in a named package (instead of the default no-name package). Let's call our package "com.xyz
".
(Text Editor) Under "WEB-INF\src
", create a sub-directory called "com
" and sub-sub-directory called "xyz
" , and save the "EntryServlet.java
" under "WEB-INF\src\com\xyz"
.
(Eclipse) TODO
(NetBeans) Expand on your project node ⇒ Right-click on "Source Packages" ⇒ New ⇒ Others ⇒ In "Categories", select "Web" ⇒ In "File Types", select "Servlet" ⇒ In "Class Name", enter "EntryServlet
" ⇒ In "Package", enter "com.xyz
" ⇒ Next ⇒ Check "Add Information to deployment descriptor (web.xml
)" ⇒ In "URL Pattern", enter "/start
" ⇒ "Finish".
(Alternatively, you can first create a new "package" called "com.xyz
" and then create the Java Servlet.)
\src\com\xyz\EntryServlet.java
package com.xyz;
import java.io.*;
import java.sql.*;
import jakarta.servlet.*; // Tomcat 10 (Jakarta EE)
import jakarta.servlet.http.*;
import java.util.logging.Logger;
import java.util.logging.Level;
import jakarta.servlet.annotation.WebServlet;
@WebServlet(urlPatterns={"/start", "/entry"}) // Configure the URL for this servlet (Tomcat 7/Servlet 3.0)
public class EntryServlet extends HttpServlet {
private String dbURL, dbUser, dbPW;
private static final Logger logger = Logger.getLogger(EntryServlet.class.getName());
@Override
public void init(ServletConfig config) throws ServletException {
// Retrieve the database-URL, dbUser, dbPW from webapp init parameters
super.init(config);
ServletContext context = config.getServletContext();
dbURL = context.getInitParameter("dbURL");
dbUser = context.getInitParameter("dbUser");
dbPW = context.getInitParameter("dbPW");
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("<!DOCTYPE html>");
out.println("<html><head><title>Welcome to YaBookShop</title></head>");
out.println("<body>");
out.println("<h2>Welcome to Yet Another E-BookShop (YaBook)</h2>");
try (
// Step 1: Create a connection to the database via a Connection object
Connection conn = DriverManager.getConnection(dbURL, dbUser, dbPW); // For MySQL
// The format is: "jdbc:mysql://hostname:port/databaseName", "dbUser", "dbPW"
// Step 2: Allocate a SQL 'Statement' object in the Connection
Statement stmt = conn.createStatement();
) {
// Step 3: Execute a SQL SELECT query
String sqlStr = "SELECT DISTINCT author FROM books WHERE qty > 0";
// System.out.println(sqlStr); // for debugging
ResultSet rset = stmt.executeQuery(sqlStr);
// Step 4: Process the query result set
// Begin an HTML form
out.println("<form method='get' action='search'>");
// A pull-down menu of all the authors with a no-selection option
out.println("Choose an Author: <select name='author' size='1'>");
out.println("<option value=''>Select...</option>"); // no-selection
while (rset.next()) { // list all the authors
String author = rset.getString("author");
out.println("<option value='" + author + "'>" + author + "</option>");
}
out.println("</select><br />");
out.println("<p>OR</p>");
// A text field for entering search word for pattern matching
out.println("Search \"Title\" or \"Author\": <input type='text' name='search' />");
// Submit and reset buttons
out.println("<br /><br />");
out.println("<input type='submit' value='SEARCH' />");
out.println("<input type='reset' value='CLEAR' />");
out.println("</form>");
} catch (SQLException ex) {
out.println("<h3>Service not available. Please try again later!</h3>");
ex.printStackTrace(); // to Tomcat's console
logger.log(Level.SEVERE, "SQL error", ex);
} // Step 5: Close conn and stmt - Done automatically by try-with-resources (JDK 7)
out.println("</body></html>");
out.close();
}
// Same response for GET and POST requests
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
How it works?
- In Line 1, we place the source file in package "
com.xyz
" to facilitate deployment.(Windows/Text Editor/CMD) As the source file is saved under "WEB-INF\src\com\xyz
", we need to use the following command to compile the source and place the resultant class in "WEB-INF\classes\com\xyz
":cd \path\to\yaebooshop\WEB-INF\classes javac -cp ..\..\..\..\lib\servlet-api.jar -d . ..\src\com\xyz\EntryServlet.java // -cp needed to include servlet-api.jar at "$TOMCAT_HOME\lib" // -d to specify the directory of the compiled class, where . denotes current directory
- In line 11, we used annotation
@WebServlet()
to define 2 URLs for this servlet,{"/start", "/entry"}
, in the form of aString
array. - Instead of hard-coding the database-URL, username and password in the
getConnection()
method, we shall keep them in the web application's initialization parameters. We will configure the init parameters later in "web.xml
". They are available to all the servlets under this web application (i.e., having application scope). In theinit()
method, which runs once when the servlet is loaded into the container, we retrieve theServletContext
viaServletConfig
, and then retrieve the initialization parameters. - In the
doGet()
method, which runs once per user request, we print an HTML form, consisting of a pull-down menu of authors (with request parameter name of "author=name
") and a search text field (with request parameter name of "search=term
"). The list of author is obtained via a database query. - The form will be submitted to a URL '
/query
", using GET request method. In production, we shall change to POST request, withdoPost()
re-directed todoGet()
. The URL triggered shall be:http://hostname:port/yaebookshop/query?author=xxx&search=yyy
- In line 15 and 74, I also replace the
printStackTrace()
with proper logging framework, for production use.
Application Deployment Descriptor "WEB-INF/web.xml
"
(NetBeans) You can create "web.xml
" by right-click on the project node ⇒ New ⇒ Others ⇒ In "Categories", select "Web" ⇒ In "File Types", select "Standard Deployment Descriptor (web.xml)". The "web.xml
" is available under the "Configuration Files" node.
WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
version="6.0"
metadata-complete="false"> <!-- set to false to also use @WebServlet() -->
<description>Yet another e-bookshop</description>
<display-name>Yet another e-bookshop</display-name>
<request-character-encoding>UTF-8</request-character-encoding>
<!-- MySQL database parameters -->
<context-param>
<param-name>dbURL</param-name>
<param-value>jdbc:mysql://localhost:3306/ebookshop</param-value>
</context-param>
<context-param>
<param-name>dbUser</param-name>
<param-value>myuser</param-value>
</context-param>
<context-param>
<param-name>dbPW</param-name>
<param-value>xxxx</param-value>
</context-param>
<!-- Use @WebServlet() annotation instead -->
<!-- Need to set <web-app>'s attribute metadata-complete="false" to use additional data -->
<!--
<servlet>
<servlet-name>EntryServlet</servlet-name>
<servlet-class>com.xyz.EntryServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>QueryServlet</servlet-name>
<servlet-class>com.xyz.QueryServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>OrderServlet</servlet-name>
<servlet-class>com.xyz.OrderServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>EntryServlet</servlet-name>
<url-pattern>/start</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>QueryServlet</servlet-name>
<url-pattern>/search</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>OrderServlet</servlet-name>
<url-pattern>/order</url-pattern>
</servlet-mapping>
-->
<session-config>
<session-timeout>30</session-timeout>
</session-config>
<welcome-file-list>
<welcome-file>start</welcome-file>
</welcome-file-list>
</web-app>
How It Works?
- This "
web.xml
" contains webapp's initialization parameters (namely, database-URL, username and password), which are available to all servlets under this webapp (i.e., having applications scope). They are make available via theServletContext
object. - We used
@WebServlet()
to configure the URL. To allow more external configurations, you need to setmetadata-complete="false"
in<web-app>
(line 7). - In line 61-63, We set the "
start
" as a welcome file. In other word, directory request tohttp://hostname:port/yaebookshop
will also start the application.
Logging Configuration File (logging.properties
)
The Java Logging requires a configuration file. Create a new file logging.properties
under "classes
" (or somewhere in classpath
), i.e., "WEB-INF\classes\logging.properties
".
handlers = java.util.logging.FileHandler
.level = FINE
# FileHandler (in $TOMCAT_HOME/logs)
java.util.logging.FileHandler.level = INFO
java.util.logging.FileHandler.pattern = ../logs/errors.log
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter
# ConsoleHandler (not used)
java.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
How It Works?
- The log file is located at "
$TOMCAT_HOME/logs
".
Handling the Query - "QueryServlet.java
" (with URL "/search
")
"com\xyz\QueryServlet.java
"
package com.xyz;
import java.io.*;
import java.sql.*;
import jakarta.servlet.*; // Tomcat 10 (Jakarta EE)
import jakarta.servlet.http.*;
import java.util.logging.Logger;
import java.util.logging.Level;
import jakarta.servlet.annotation.WebServlet;
@WebServlet(urlPatterns={"/search"}) // Configure the URL for this servlet (Tomcat 7/Servlet 3.0)
public class QueryServlet extends HttpServlet {
private String dbURL, dbUser, dbPW;
private static final Logger logger = Logger.getLogger(QueryServlet.class.getName());
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
ServletContext context = config.getServletContext();
dbURL = context.getInitParameter("dbURL");
dbUser = context.getInitParameter("dbUser");
dbPW = context.getInitParameter("dbPW");
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("<!DOCTYPE html>");
out.println("<html><head><title>Query Results</title></head>");
out.println("<body>");
out.println("<h2>YaBook - Query Results</h2>");
// Retrieve and process request parameters: "author" and "search"
String author = request.getParameter("author").trim();
boolean hasAuthorParam = (author != null) && (!author.equals("Select..."));
String searchWord = request.getParameter("search").trim();
boolean hasSearchParam = (searchWord != null) && (searchWord.length() > 0);
// Validate inputs and Handling errors
if (!hasAuthorParam && !hasSearchParam) { // No params present
out.println("<h3>Please select an author OR enter a search term!</h3>");
out.println("<p><a href='start'>Back to Main Menu</a></p>");
out.println("</body></html>");
return;
}
try (
// Step 1: Create a connection to the database via a Connection object
Connection conn = DriverManager.getConnection(dbURL, dbUser, dbPW); // For MySQL
// The format is: "jdbc:mysql://hostname:port/databaseName", "dbUser", "dbPW"
// Step 2: Allocate a SQL 'Statement' object in the Connection
Statement stmt = conn.createStatement();
) {
// Step 3: Execute a SQL SELECT query
// Form a SQL command based on the param(s) present
StringBuilder sqlStr = new StringBuilder(); // more efficient than String
sqlStr.append("SELECT * FROM books WHERE qty > 0 AND (");
if (hasAuthorParam) {
sqlStr.append("author = '").append(author).append("'");
}
if (hasSearchParam) {
if (hasAuthorParam) {
sqlStr.append(" OR ");
}
sqlStr.append("author LIKE '%").append(searchWord)
.append("%' OR title LIKE '%").append(searchWord).append("%'");
}
sqlStr.append(") ORDER BY author, title");
//System.out.println(sqlStr); // for debugging
ResultSet rset = stmt.executeQuery(sqlStr.toString());
// Step 4: Process the query result set
if (!rset.next()) { // Check for empty ResultSet (no book found)
out.println("<h3>No book found. Please try again!</h3>");
out.println("<p><a href='start'>Back to Main Menu</a></p>");
} else {
// Print the result in an HTML form inside a table
out.println("<form method='get' action='order'>");
out.println("<table border='1' cellpadding='6'>");
out.println("<tr>");
out.println("<th> </th>");
out.println("<th>AUTHOR</th>");
out.println("<th>TITLE</th>");
out.println("<th>PRICE</th>");
out.println("<th>QTY</th>");
out.println("</tr>");
// ResultSet's cursor now pointing at first row
do {
// Print each row with a checkbox identified by book's id
String id = rset.getString("id");
out.println("<tr>");
out.println("<td><input type='checkbox' name='id' value='" + id + "' /></td>");
out.println("<td>" + rset.getString("author") + "</td>");
out.println("<td>" + rset.getString("title") + "</td>");
out.println("<td>$" + rset.getString("price") + "</td>");
out.println("<td><input type='text' size='3' value='1' name='qty" + id + "' /></td>");
out.println("</tr>");
} while (rset.next());
out.println("</table><br />");
// Ask for name, email and phone using text fields (arranged in a table)
out.println("<table>");
out.println("<tr><td>Enter your Name:</td>");
out.println("<td><input type='text' name='cust_name' required /></td></tr>");
out.println("<tr><td>Enter your Email (user@host):</td>");
out.println("<td><input type='email' name='cust_email' required /></td></tr>");
out.println("<tr><td>Enter your Phone Number (8-digit):</td>");
out.println("<td><input type='text' required pattern='\\d{8}' name='cust_phone' /></td></tr></table><br />");
// Submit and reset buttons
out.println("<input type='submit' value='ORDER' />");
out.println("<input type='reset' value='CLEAR' /></form>");
// Hyperlink to go back to search menu
out.println("<p><a href='start'>Back to Main Menu</a></p>");
}
} catch (SQLException ex) {
out.println("<h3>Service not available. Please try again later!</h3>");
ex.printStackTrace(); // to Tomcat's console
logger.log(Level.SEVERE, "SQL error", ex);
} // Step 5: Close conn and stmt - Done automatically by try-with-resources (JDK 7)
out.println("</body></html>");
out.close();
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
How It Works?
- This servlet retrieve the query parameters
author
andsearch
to form a SQL SELECT query. Take note that many lines of codes are used to check the validity of input parameters. The SQL SELECT statement is as follows. We use pattern matching (LIKE operator) to handle the search term.SELECT * FROM books WHERE qty > 0 AND (author = 'name' OR author LIKE '%xxx%' OR title LIKE '%yyy%')
- The servlet outputs an HTML form, listing all the books selected, with a checkbox for ordering and a text field for entering the quantity. The
name=value
pair of the checkbox is theid=xxx
; whereas thename=value
pair of the quantity text field isqty+id=qtyOrdered
, for example,id=1001
andqty1001=5
. - It also prints three text fields to prompt for the customer's name, email and phone number, with
name
attribute ofcust_name
,cust_email
andcust_phone
, respectively. - The form is to be submitted to URL "
/order
" using GET request (shall change to POST for production).
Handling the Order - "OrderServlet.java
" (with URL "/order
")
"com\xyz\OrderServlet.java
"
package com.xyz;
import java.io.*;
import java.sql.*;
import jakarta.servlet.*; // Tomcat 10 (Jakarta EE)
import jakarta.servlet.http.*;
import java.util.logging.Logger;
import java.util.logging.Level;
import jakarta.servlet.annotation.WebServlet;
import java.util.regex.Pattern;
@WebServlet(urlPatterns={"/order"}) // Configure the URL for this servlet (Tomcat 7/Servlet 3.0)
public class OrderServlet extends HttpServlet {
private String dbURL, dbUser, dbPW;
private static final Logger logger = Logger.getLogger(OrderServlet.class.getName());
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
ServletContext context = config.getServletContext();
dbURL = context.getInitParameter("dbURL");
dbUser = context.getInitParameter("dbUser");
dbPW = context.getInitParameter("dbPW");
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("<!DOCTYPE html>");
out.println("<html><head><title>Order Confirmation</title></head>");
out.println("<body>");
out.println("<h2>YaBook - Order Confirmation</h2>");
// Validate and process request parameters: id(s), cust_name, cust_email, cust_phone
boolean inputError = false;
String[] ids = request.getParameterValues("id"); // Possibly more than one values
if (ids == null || ids.length == 0) {
out.println("<h3>Please Select a Book!</h3>");
inputError = true;
}
String custName = request.getParameter("cust_name").trim();
if (custName == null || (custName.length() == 0)) {
out.println("<h3>Please Enter Your Name!</h3>");
inputError = true;
}
String custEmail = request.getParameter("cust_email").trim();
if (custEmail == null || (custEmail.indexOf('@') == -1)) {
out.println("<h3>Please Enter Your e-mail (user@host)!!!</h3>");
inputError = true;
}
String custPhone = request.getParameter("cust_phone").trim();
if (custPhone == null || !Pattern.matches("\\d{8}", custPhone)) {
out.println("<h3>Please Enter an 8-digit Phone Number!</h3>");
inputError = true;
}
if (inputError) {
out.println("<p><a href='start'>Back to Main Menu</a></p>");
out.println("</body></html>");
return;
}
try (
// Step 1: Create a connection to the database via a Connection object
Connection conn = DriverManager.getConnection(dbURL, dbUser, dbPW); // For MySQL
// The format is: "jdbc:mysql://hostname:port/databaseName", "dbUser", "dbPW"
// Step 2: Allocate a SQL 'Statement' object in the Connection
Statement stmt = conn.createStatement();
) {
// Display the name, email and phone (arranged in a table)
out.println("<table>");
out.println("<tr><td>Customer Name:</td><td>" + custName + "</td></tr>");
out.println("<tr><td>Customer Email:</td><td>" + custEmail + "</td></tr>");
out.println("<tr><td>Customer Phone Number:</td><td>" + custPhone + "</td></tr></table>");
// Print the book(s) ordered in a table
out.println("<br />");
out.println("<table border='1' cellpadding='6'>");
out.println("<tr><th>AUTHOR</th><th>TITLE</th><th>PRICE</th><th>QTY</th></tr>");
// Step 3 and 4: Execute a SQL SELECT query and Process the result
float totalPrice = 0f;
String sqlStr = null;
ResultSet rset = null;
for (String id : ids) {
sqlStr = "SELECT * FROM books WHERE id = " + id;
//System.out.println(sqlStr); // for debugging
rset = stmt.executeQuery(sqlStr);
// Expect only one row in ResultSet
rset.next();
int qtyAvailable = rset.getInt("qty");
String title = rset.getString("title");
String author = rset.getString("author");
float price = rset.getFloat("price");
int qtyOrdered = Integer.parseInt(request.getParameter("qty" + id));
if (qtyAvailable < qtyOrdered) {
out.println("<tr>");
out.println("<td>" + author + "</td>");
out.println("<td>" + title + "</td>");
out.println("<td>" + price + "</td>");
out.println("<td>" + "Quantity exceeded" + "</td></tr>");
} else {
sqlStr = "UPDATE books SET qty = qty - " + qtyOrdered + " WHERE id = " + id;
// System.out.println(sqlStr); // for debugging
stmt.executeUpdate(sqlStr);
sqlStr = "INSERT INTO order_records values ("
+ id + ", " + qtyOrdered + ", '" + custName + "', '"
+ custEmail + "', '" + custPhone + "')";
// System.out.println(sqlStr); // for debugging
stmt.executeUpdate(sqlStr);
// Display this book ordered
out.println("<tr>");
out.println("<td>" + author + "</td>");
out.println("<td>" + title + "</td>");
out.println("<td>" + price + "</td>");
out.println("<td>" + qtyOrdered + "</td></tr>");
totalPrice += price * qtyOrdered;
}
}
out.println("<tr><td colspan='4' align='right'>Total Price: $");
out.printf("%.2f</td></tr>", totalPrice);
out.println("</table>");
out.println("<h3>Thank you.</h3>");
out.println("<p><a href='start'>Back to Main Menu</a></p>");
} catch (SQLException ex) {
out.println("<h3>Service not available. Please try again later!</h3>");
ex.printStackTrace(); // to Tomcat's console
logger.log(Level.SEVERE, "SQL error", ex);
} // Step 5: Close conn and stmt - Done automatically by try-with-resources (JDK 7)
out.println("</body></html>");
out.close();
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
How It Works?
- The servlet retrieves the request parameters, such as
id=1001
,qty1001=1
,id=1002
,qty1002=2
and update the tablebooks
by reducing the quantity available. It also insert a transaction records in the tableorder_records
.
Rewrite the "OrderServlet
" to do More Checking
We shall rewrite the "OrderServlet
" to do more checking on inputs and handle the abnormal conditions:
- The phone number must be exactly 8 numeric digits.
- The quantity ordered shall be less than or equal to quantity available.
- Replace special HTML characters
>
,<
,&
and"
with escape sequences in request parameterscust_name
,cust_email
. - I also disable the auto-commit, which commit every SQL statement. Instead, a commit is issued only if the entire order (i.e., all books) can be met. Partial update will be roll-backed.
- [TODO] more
Helper Utility "com.xyz.InputFilter
"
The utility class InputFilter
provide these static
methods to filter or verify the inputs entered by the client.
htmlFilter
: replace special HTML characters>
,<
,&
and"
with escape sequences.isValidPhone
: Check if the given input string is a valid phone number (e.g., 8-digit).parsePositiveInt
: Parse the given input string to a positive integer or zero otherwise. Useful for checking the quantity ordered.- [TODO] more
package com.xyz;
import java.util.regex.*;
public final class InputFilter {
/**
* Filter the specified message string for characters that are sensitive
* in HTML. This avoids potential attacks caused by including JavaScript
* codes in the request URL that is often reported in error messages.
*
* "Apache Common Text" API provides an utility called StringEscapeUtils.escapeHtml4().
*/
public static String htmlFilter(String message) {
if (message == null) return null;
int len = message.length();
StringBuilder result = new StringBuilder(len + 20);
char aChar;
for (int i = 0; i < len; ++i) {
aChar = message.charAt(i);
switch (aChar) {
case '<':
result.append("<"); break;
case '>':
result.append(">"); break;
case '&':
result.append("&"); break;
case '"':
result.append("""); break;
default:
result.append(aChar);
}
}
return (result.toString());
}
/**
* Given a phone number string, return true if it is an 8-digit number
* Use java.util.regex API.
*/
public static boolean isValidPhone(String phoneNumber) {
return Pattern.matches("\\d{8}", phoneNumber);
}
/**
* Given a string, return a positive integer if the string can be parsed into
* a positive integer. Return 0 for non-positive integer or parsing error.
*/
public static int parsePositiveInt(String str) {
if (str == null || (str = str.trim()).length() == 0) return 0;
int result;
try {
result = Integer.parseInt(str);
} catch (NumberFormatException ex) {
return 0;
}
return (result > 0) ? result : 0;
}
}
"OrderServlet.java
" (re-written)
[TODO]
Database Connection Pooling
Using a new connection to service each request is highly inefficient, due to the high overhead involved in initializing and maintaining the connection. A common practice is to setup a common pool of database connections. A servlet picks up an available connection from the pool to perform database operation, and returns the connection to the pool once it is done. Tomcat supports database connection pooling via JNDI (Java Naming and Directory Interface).
Create a new web application called "yaebsdbcp
" (yet another e-bookshop database connection pooling). Copy all the servlets in the previous exercises into this webapp.
The steps to set up database connection pooling in Tomcat is as follows:
Step 1: Configure a JNDI DataSource "META-INF\context.xml
"
Write a JNDI (Java Naming and Directory Interface) DataSource configuration in "context.xml
" as follows. For application-specific configuration, save it under application's "META-INF
". (For server-wide configuration, put the <Resource>
element under <GlobalNamingResources>
in $CATALINA_HOME\conf\server.xml
.)
(NetBeans) The "context.xml
" can be found under the "Configuration Files" node.
<?xml version='1.0' encoding='UTF-8' ?> <Context reloadable="true"> <!-- maxActive: Maximum number of dB connections in pool. Set to -1 for no limit. maxIdle: Maximum number of idle dB connections to retain in pool. Set to -1 for no limit. maxWait: Maximum milliseconds to wait for a dB connection to become available Set to -1 to wait indefinitely. --> <Resource name="jdbc/mysql_ebookshop" auth="Container" type="javax.sql.DataSource" maxActive="100" maxIdle="30" maxWait="10000" removeAbandoned="true" username="myuser" password="xxxx" driverClassName="com.mysql.jdbc.Driver" url="jdbc:mysql://localhost:3306/ebookshop" /> </Context>
The above configuration declares a JNDI resource name called "jdbc/mysql_ebookshop
" corresponds to the MySQL connection "mysql://localhost:3306/ebookshop
". Check your database-url, username and password.
Step 2: Application Deployment Descriptor "web.xml
"
Configure "WEB-INF\web.xml
" to reference the JNDI "jdbc/mysql_ebookshop
".
<web-app ......> <resource-ref> <description>DB Connection Pool</description> <res-ref-name>jdbc/mysql_ebookshop</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> </resource-ref> ...... </web-app>
Note: This step seems to be optional?!
Step 3: Modify your Servlet to Use Connection Pool
Modify all the servlet to use database connection pooling as follows:
...... public class EntryServlet extends HttpServlet { private DataSource pool; // Database connection pool @Override public void init(ServletConfig config) throws ServletException { try { // Create a JNDI Initial context to be able to lookup the DataSource InitialContext ctx = new InitialContext(); // Lookup the DataSource, which will be backed by a pool // that the application server provides. pool = (DataSource)ctx.lookup("java:comp/env/jdbc/mysql_ebookshop"); if (pool == null) throw new ServletException("Unknown DataSource 'jdbc/mysql_ebookshop'"); } catch (NamingException ex) { ex.printStackTrace(); Logger.getLogger(EntryServlet.class.getName()).log(Level.SEVERE, null, ex); } } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); try ( // Step 1: Create a connection to the database via a Connection object Connection conn = pool.getConnection(); // For MySQL // The format is: "jdbc:mysql://hostname:port/databaseName", "dbUser", "dbPW" // Step 2: Allocate a SQL 'Statement' object in the Connection Statement stmt = conn.createStatement(); ) { ...... } // conn.close() returns the Connection back to the pool } ...... }
How It Works?
- The
init()
method looks up the JNDI directory and setup the database connection pool (DataSource
) as configured in the<context.xml>
. - In
doGet()
,pool.getConnection()
gets an available connection from the pool. Theconn.close()
is important now, as it returns the connection back to the pool.
Modify all the servlets (EntryServlet
, QueryServlet
, and OrderServlet
) to use connection pooling.
[TODO] How to monitor the connection pool?
Shopping Cart and Session Management
(After Note) This example uses Java classes to maintain shopping cart. In production, it is better to use a database table to maintain the shopping cart, for persistent and simplicity. See the next section on "User and Role management".
HTTP is a stateless protocol. In other words, the current request does not know what has been done in the previous requests. This creates a problem for applications that runs over many requests, such as online shopping. You need to maintain a so-called session with a shopping cart to pass data among the multiple requests. The user will check out his/her shopping cart once he/she is done.
Java Servlet API provide a HttpSession
that greatly simplifies the session management.
For details on session management, read "Java Servlets - Session Tracking".
E-Shop with Shopping Cart
Create a New Webapp
Create a new webapp called "yaebscart
" (yet another e-bookshop with shopping cart). We shall re-use codes in database connection pooling exercise.
Sequence Diagram
- A client issues a URL
http://hostname:port/yaebscart/start
to start the webapp, which is mapped to "EntryServlet
". The servlet responses with an HTML query form. The form is to be submitted to URL "/search
" (mapped to "QueryServlet
") with request parametersauthor=name
and/orsearch=term
. - The client checks the item(s) and sends the request to "
QueryServlet
". The servlet extracts the request parameters, queries the database, and returns the list of books in an HTML form. Each item has a checkbox (withid=xxx
andqtyxxx=yyy
). The checkboxes are enclosed within a form with a hidden inputtodo=add
. The form is to be submitted to URL "\cart
" (mapped is "CartServlet
"). - The "
CartServlet
" create a newHttpSession
and aCart
(during the first access), and places theCart
inside theHttpSession
. TheCartServlet
handle these processes:todo=add, id=1001, qty1001=2, [id=1002, qty1002-3...]
: Add the books into the shopping cart. If the book is already in the cart, increase its quantity ordered. Display the shopping cart.todo=remove, id=1001
: Remove the book (identified by the book's id) from the shopping cart. Display the shopping cart.todo=update, id=1001, qty1000=5
: Update the quantity ordered for that particular book's id, in the shopping cart. Display the shopping cart.todo=view
: Display the shopping cart.
- The "
CheckoutServlet
" performs the checkout process. It retrieves the orders form the shopping cart, updates the database tablesbooks
by reducing the quantity available, and inserts a transaction record inorder_records
table.
Create the Java Classes to Support Shopping Cart - Cart
and CartItem
The CartItem
class models individual item placed inside the shopping cart. In our e-bookstore, we need to keep track of the id, title, author, price and quantity ordered.
package com.xyz; /** * The class CartItem models an item in the Cart. * This class shall not be accessed by the controlling logic directly. * Instead Use Cart.add() or Cart.remove() to add or remove an item from the Cart. */ public class CartItem { private int id; private String title; private String author; private float price; private int qtyOrdered; // Constructor public CartItem(int id, String title, String author, float price, int qtyOrdered) { this.id = id; this.title = title; this.author = author; this.price = price; this.qtyOrdered = qtyOrdered; } public int getId() { return id; } public String getAuthor() { return author; } public String getTitle() { return title; } public float getPrice() { return price; } public int getQtyOrdered() { return qtyOrdered; } public void setQtyOrdered(int qtyOrdered) { this.qtyOrdered = qtyOrdered; } }
The Cart
class stores the items in a List
of CartItem
. It also provides these public methods:
add()
: Add a item into the card. It checks if theid
is already in the cart. If so, it adjust the quantity ordered. Otherwise, it creates a newCartItem
and adds to theArrayList
.update()
: Update the quantity order for the given book's id.remove()
: Remove a particular item from the shopping cart, identified via the book's id.isEmpty()
: Return true if the cart is empty.size()
: Return the number of items in the shopping cart.getItems()
: Return all the items of the shopping cart in aList<CartItem>
.clear()
: Empty the contents of the shopping cart.
package com.xyz; import java.util.ArrayList; import java.util.Iterator; import java.util.List; /** * The Cart class models the shopping cart, which contains CartItem. * It also provides method to add() and remove() a CartItem. */ public class Cart { private List<CartItem> cart; // List of CartItems // constructor public Cart() { cart = new ArrayList<CartItem>(); } // Add a CartItem into this Cart public void add(int id, String title, String author, float price, int qtyOrdered) { // Check if the id is already in the shopping cart Iterator<CartItem> iter = cart.iterator(); while (iter.hasNext()) { CartItem item = iter.next(); if (item.getId() == id) { // id found, increase qtyOrdered item.setQtyOrdered(item.getQtyOrdered() + qtyOrdered); return; } } // id not found, create a new CartItem cart.add(new CartItem(id, title, author, price, qtyOrdered)); } // Update the quantity for the given id public boolean update(int id, int newQty) { Iterator<CartItem> iter = cart.iterator(); while (iter.hasNext()) { CartItem item = iter.next(); if (item.getId() == id) { // id found, increase qtyOrdered item.setQtyOrdered(newQty); return true; } } return false; } // Remove a CartItem given its id public void remove(int id) { Iterator<CartItem> iter = cart.iterator(); while (iter.hasNext()) { CartItem item = iter.next(); if (item.getId() == id) { cart.remove(item); return; } } } // Get the number of CartItems in this Cart public int size() { return cart.size(); } // Check if this Cart is empty public boolean isEmpty() { return size() == 0; } // Return all the CartItems in a List<CartItem> public List<CartItem> getItems() { return cart; } // Remove all the items in this Cart public void clear() { cart.clear(); } }
EntryServlet.java
(URL "/start
")
package com.xyz; import java.io.*; import java.sql.*; import java.util.logging.*; import javax.naming.*; import javax.servlet.*; import javax.servlet.http.*; import javax.sql.DataSource; public class EntryServlet extends HttpServlet { private DataSource pool; // Database connection pool @Override public void init(ServletConfig config) throws ServletException { try { // Create a JNDI Initial context to be able to lookup the DataSource InitialContext ctx = new InitialContext(); // Lookup the DataSource pool = (DataSource)ctx.lookup("java:comp/env/jdbc/mysql_ebookshop"); if (pool == null) throw new ServletException("Unknown DataSource 'jdbc/mysql_ebookshop'"); } catch (NamingException ex) { Logger.getLogger(EntryServlet.class.getName()).log(Level.SEVERE, null, ex); } } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); Connection conn = null; Statement stmt = null; try { conn = pool.getConnection(); // Get a connection from the pool stmt = conn.createStatement(); String sqlStr = "SELECT DISTINCT author FROM books WHERE qty > 0"; ResultSet rset = stmt.executeQuery(sqlStr); out.println("<html><head><title>Welcome to YaEshop</title></head><body>"); out.println("<h2>Welcome to Yet Another E-BookShop</h2>"); out.println("<form method='get' action='search'>"); // A pull-down menu of all the authors with a no-selection option out.println("Choose an Author: <select name='author' size='1'>"); out.println("<option value=''>Select...</option>"); // no-selection while (rset.next()) { // list all the authors String author = rset.getString("author"); out.println("<option value='" + author + "'>" + author + "</option>"); } out.println("</select><br />"); out.println("<p>OR</p>"); // A text field for entering search word for pattern matching out.println("Search \"Title\" or \"Author\": <input type='text' name='search' />"); // Submit and reset buttons out.println("<br /><br />"); out.println("<input type='submit' value='SEARCH' />"); out.println("<input type='reset' value='CLEAR' />"); out.println("</form>"); // Show "View Shopping Cart" if the cart is not empty HttpSession session = request.getSession(false); // check if session exists if (session != null) { Cart cart; synchronized (session) { // Retrieve the shopping cart for this session, if any. Otherwise, create one. cart = (Cart) session.getAttribute("cart"); if (cart != null && !cart.isEmpty()) { out.println("<P><a href='cart?todo=view'>View Shopping Cart</a></p>"); } } } out.println("</body></html>"); } catch (SQLException ex) { out.println("<h3>Service not available. Please try again later!</h3></body></html>"); Logger.getLogger(EntryServlet.class.getName()).log(Level.SEVERE, null, ex); } finally { out.close(); try { if (stmt != null) stmt.close(); if (conn != null) conn.close(); // Return the connection to the pool } catch (SQLException ex) { Logger.getLogger(EntryServlet.class.getName()).log(Level.SEVERE, null, ex); } } } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }
QueryServlet.java
(URL "/query
")
package com.xyz; import java.io.*; import java.sql.*; import java.util.logging.*; import javax.naming.*; import javax.servlet.*; import javax.servlet.http.*; import javax.sql.*; public class QueryServlet extends HttpServlet { private DataSource pool; // Database connection pool @Override public void init(ServletConfig config) throws ServletException { try { // Create a JNDI Initial context to be able to lookup the DataSource InitialContext ctx = new InitialContext(); // Lookup the DataSource. pool = (DataSource)ctx.lookup("java:comp/env/jdbc/mysql_ebookshop"); if (pool == null) throw new ServletException("Unknown DataSource 'jdbc/mysql_ebookshop'"); } catch (NamingException ex) { Logger.getLogger(EntryServlet.class.getName()).log(Level.SEVERE, null, ex); } } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); Connection conn = null; Statement stmt = null; try { // Retrieve and process request parameters: "author" and "search" String author = request.getParameter("author"); boolean hasAuthorParam = author != null && !author.equals("Select..."); String searchWord = request.getParameter("search").trim(); boolean hasSearchParam = searchWord != null && (searchWord.length() > 0); out.println("<html><head><title>Query Results</title></head><body>"); out.println("<h2>YAEBS - Query Results</h2>"); if (!hasAuthorParam && !hasSearchParam) { // No params present out.println("<h3>Please select an author or enter a search term!</h3>"); out.println("<p><a href='start'>Back to Select Menu</a></p>"); } else { conn = pool.getConnection(); stmt = conn.createStatement(); // Form a SQL command based on the param(s) present StringBuilder sqlStr = new StringBuilder(); // more efficient than String sqlStr.append("SELECT * FROM books WHERE qty > 0 AND ("); if (hasAuthorParam) { sqlStr.append("author = '").append(author).append("'"); } if (hasSearchParam) { if (hasAuthorParam) { sqlStr.append(" OR "); } sqlStr.append("author LIKE '%").append(searchWord) .append("%' OR title LIKE '%").append(searchWord).append("%'"); } sqlStr.append(") ORDER BY author, title"); //System.out.println(sqlStr); // for debugging ResultSet rset = stmt.executeQuery(sqlStr.toString()); if (!rset.next()) { // Check for empty ResultSet (no book found) out.println("<h3>No book found. Please try again!</h3>"); out.println("<p><a href='start'>Back to Select Menu</a></p>"); } else { // Print the result in an HTML form inside a table out.println("<form method='get' action='cart'>"); out.println("<input type='hidden' name='todo' value='add' />"); out.println("<table border='1' cellpadding='6'>"); out.println("<tr>"); out.println("<th> </th>"); out.println("<th>AUTHOR</th>"); out.println("<th>TITLE</th>"); out.println("<th>PRICE</th>"); out.println("<th>QTY</th>"); out.println("</tr>"); // ResultSet's cursor now pointing at first row do { // Print each row with a checkbox identified by book's id String id = rset.getString("id"); out.println("<tr>"); out.println("<td><input type='checkbox' name='id' value='" + id + "' /></td>"); out.println("<td>" + rset.getString("author") + "</td>"); out.println("<td>" + rset.getString("title") + "</td>"); out.println("<td>$" + rset.getString("price") + "</td>"); out.println("<td><input type='text' size='3' value='1' name='qty" + id + "' /></td>"); out.println("</tr>"); } while (rset.next()); out.println("</table><br />"); // Submit and reset buttons out.println("<input type='submit' value='Add to My Shopping Cart' />"); out.println("<input type='reset' value='CLEAR' /></form>"); // Hyperlink to go back to search menu out.println("<p><a href='start'>Back to Select Menu</a></p>"); // Show "View Shopping Cart" if cart is not empty HttpSession session = request.getSession(false); // check if session exists if (session != null) { Cart cart; synchronized (session) { // Retrieve the shopping cart for this session, if any. Otherwise, create one. cart = (Cart) session.getAttribute("cart"); if (cart != null && !cart.isEmpty()) { out.println("<p><a href='cart?todo=view'>View Shopping Cart</a></p>"); } } } out.println("</body></html>"); } } } catch (SQLException ex) { out.println("<h3>Service not available. Please try again later!</h3></body></html>"); Logger.getLogger(QueryServlet.class.getName()).log(Level.SEVERE, null, ex); } finally { out.close(); try { if (stmt != null) stmt.close(); if (conn != null) conn.close(); // Return the connection to the pool } catch (SQLException ex) { Logger.getLogger(QueryServlet.class.getName()).log(Level.SEVERE, null, ex); } } } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }
CartServlet.java
(URL "/cart
")
package com.xyz; import java.io.*; import java.sql.*; import java.util.logging.*; import javax.naming.*; import javax.servlet.*; import javax.servlet.http.*; import javax.sql.DataSource; public class CartServlet extends HttpServlet { private DataSource pool; // Database connection pool @Override public void init(ServletConfig config) throws ServletException { try { // Create a JNDI Initial context to be able to lookup the DataSource InitialContext ctx = new InitialContext(); // Lookup the DataSource. pool = (DataSource)ctx.lookup("java:comp/env/jdbc/mysql_ebookshop"); if (pool == null) throw new ServletException("Unknown DataSource 'jdbc/mysql_ebookshop'"); } catch (NamingException ex) { Logger.getLogger(EntryServlet.class.getName()).log(Level.SEVERE, null, ex); } } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); // Retrieve current HTTPSession object. If none, create one. HttpSession session = request.getSession(true); Cart cart; synchronized (session) { // synchronized to prevent concurrent updates // Retrieve the shopping cart for this session, if any. Otherwise, create one. cart = (Cart) session.getAttribute("cart"); if (cart == null) { // No cart, create one. cart = new Cart(); session.setAttribute("cart", cart); // Save it into session } } Connection conn = null; Statement stmt = null; ResultSet rset = null; String sqlStr = null; try { conn = pool.getConnection(); // Get a connection from the pool stmt = conn.createStatement(); out.println("<html><head><title>Shopping Cart</title></head><body>"); out.println("<h2>YAEBS - Your Shopping Cart</h2>"); // This servlet handles 4 cases: // (1) todo=add id=1001 qty1001=5 [id=1002 qty1002=1 ...] // (2) todo=update id=1001 qty1001=5 // (3) todo=remove id=1001 // (4) todo=view String todo = request.getParameter("todo"); if (todo == null) todo = "view"; // to prevent null pointer if (todo.equals("add") || todo.equals("update")) { // (1) todo=add id=1001 qty1001=5 [id=1002 qty1002=1 ...] // (2) todo=update id=1001 qty1001=5 String[] ids = request.getParameterValues("id"); if (ids == null) { out.println("<h3>Please Select a Book!</h3></body></html>"); return; } for (String id : ids) { sqlStr = "SELECT * FROM books WHERE id = " + id; //System.out.println(sqlStr); // for debugging rset = stmt.executeQuery(sqlStr); rset.next(); // Expect only one row in ResultSet String title = rset.getString("title"); String author = rset.getString("author"); float price = rset.getFloat("price"); // Get quantity ordered - no error check! int qtyOrdered = Integer.parseInt(request.getParameter("qty" + id)); int idInt = Integer.parseInt(id); if (todo.equals("add")) { cart.add(idInt, title, author, price, qtyOrdered); } else if (todo.equals("update")) { cart.update(idInt, qtyOrdered); } } } else if (todo.equals("remove")) { String id = request.getParameter("id"); // Only one id for remove case cart.remove(Integer.parseInt(id)); } // All cases - Always display the shopping cart if (cart.isEmpty()) { out.println("<p>Your shopping cart is empty</p>"); } else { out.println("<table border='1' cellpadding='6'>"); out.println("<tr>"); out.println("<th>AUTHOR</th>"); out.println("<th>TITLE</th>"); out.println("<th>PRICE</th>"); out.println("<th>QTY</th>"); out.println("<th>REMOVE</th></tr>"); float totalPrice = 0f; for (CartItem item : cart.getItems()) { int id = item.getId(); String author = item.getAuthor(); String title = item.getTitle(); float price = item.getPrice(); int qtyOrdered = item.getQtyOrdered(); out.println("<tr>"); out.println("<td>" + author + "</td>"); out.println("<td>" + title + "</td>"); out.println("<td>" + price + "</td>"); out.println("<td><form method='get'>"); out.println("<input type='hidden' name='todo' value='update' />"); out.println("<input type='hidden' name='id' value='" + id + "' />"); out.println("<input type='text' size='3' name='qty" + id + "' value='" + qtyOrdered + "' />" ); out.println("<input type='submit' value='Update' />"); out.println("</form></td>"); out.println("<td><form method='get'>"); out.println("<input type='submit' value='Remove'>"); out.println("<input type='hidden' name='todo' value='remove'"); out.println("<input type='hidden' name='id' value='" + id + "'>"); out.println("</form></td>"); out.println("</tr>"); totalPrice += price * qtyOrdered; } out.println("<tr><td colspan='5' align='right'>Total Price: $"); out.printf("%.2f</td></tr>", totalPrice); out.println("</table>"); } out.println("<p><a href='start'>Select More Books...</a></p>"); // Display the Checkout if (!cart.isEmpty()) { out.println("<br /><br />"); out.println("<form method='get' action='checkout'>"); out.println("<input type='submit' value='CHECK OUT'>"); out.println("<p>Please fill in your particular before checking out:</p>"); out.println("<table>"); out.println("<tr>"); out.println("<td>Enter your Name:</td>"); out.println("<td><input type='text' name='cust_name' /></td></tr>"); out.println("<tr>"); out.println("<td>Enter your Email:</td>"); out.println("<td><input type='text' name='cust_email' /></td></tr>"); out.println("<tr>"); out.println("<td>Enter your Phone Number:</td>"); out.println("<td><input type='text' name='cust_phone' /></td></tr>"); out.println("</table>"); out.println("</form>"); } out.println("</body></html>"); } catch (SQLException ex) { out.println("<h3>Service not available. Please try again later!</h3></body></html>"); Logger.getLogger(CartServlet.class.getName()).log(Level.SEVERE, null, ex); } finally { out.close(); try { if (stmt != null) stmt.close(); if (conn != null) conn.close(); // return the connection to the pool } catch (SQLException ex) { Logger.getLogger(CartServlet.class.getName()).log(Level.SEVERE, null, ex); } } } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }
CheckoutServlet.java
(URL "/checkout
")
package com.xyz; import java.io.*; import java.sql.*; import java.util.logging.*; import javax.naming.*; import javax.servlet.*; import javax.servlet.http.*; import javax.sql.DataSource; public class CheckoutServlet extends HttpServlet { private DataSource pool; // Database connection pool @Override public void init(ServletConfig config) throws ServletException { try { // Create a JNDI Initial context to be able to lookup the DataSource InitialContext ctx = new InitialContext(); // Lookup the DataSource. pool = (DataSource)ctx.lookup("java:comp/env/jdbc/mysql_ebookshop"); if (pool == null) throw new ServletException("Unknown DataSource 'jdbc/mysql_ebookshop'"); } catch (NamingException ex) { Logger.getLogger(EntryServlet.class.getName()).log(Level.SEVERE, null, ex); } } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); Connection conn = null; Statement stmt = null; ResultSet rset = null; String sqlStr = null; HttpSession session = null; Cart cart = null; try { conn = pool.getConnection(); // Get a connection from the pool stmt = conn.createStatement(); out.println("<html><head><title>Checkout</title></head><body>"); out.println("<h2>YAEBS - Checkout</h2>"); // Retrieve the Cart session = request.getSession(false); if (session == null) { out.println("<h3>Your Shopping cart is empty!</h3></body></html>"); return; } synchronized (session) { cart = (Cart) session.getAttribute("cart"); if (cart == null) { out.println("<h3>Your Shopping cart is empty!</h3></body></html>"); return; } } // Retrieve and process request parameters: id(s), cust_name, cust_email, cust_phone String custName = request.getParameter("cust_name"); boolean hasCustName = custName != null && ((custName = custName.trim()).length() > 0); String custEmail = request.getParameter("cust_email").trim(); boolean hasCustEmail = custEmail != null && ((custEmail = custEmail.trim()).length() > 0); String custPhone = request.getParameter("cust_phone").trim(); boolean hasCustPhone = custPhone != null && ((custPhone = custPhone.trim()).length() > 0); // Validate inputs if (!hasCustName) { out.println("<h3>Please Enter Your Name!</h3></body></html>"); return; } else if (!hasCustEmail || (custEmail.indexOf('@') == -1)) { out.println("<h3>Please Enter Your email (user@host)!</h3></body></html>"); return; } else if (!hasCustPhone || custPhone.length() != 8) { out.println("<h3>Please Enter an 8-digit Phone Number!</h3></body></html>"); return; } // Display the name, email and phone (arranged in a table) out.println("<table>"); out.println("<tr>"); out.println("<td>Customer Name:</td>"); out.println("<td>" + custName + "</td></tr>"); out.println("<tr>"); out.println("<td>Customer Email:</td>"); out.println("<td>" + custEmail + "</td></tr>"); out.println("<tr>"); out.println("<td>Customer Phone Number:</td>"); out.println("<td>" + custPhone + "</td></tr>"); out.println("</table>"); // Print the book(s) ordered in a table out.println("<br />"); out.println("<table border='1' cellpadding='6'>"); out.println("<tr>"); out.println("<th>AUTHOR</th>"); out.println("<th>TITLE</th>"); out.println("<th>PRICE</th>"); out.println("<th>QTY</th></tr>"); float totalPrice = 0f; for (CartItem item : cart.getItems()) { int id = item.getId(); String author = item.getAuthor(); String title = item.getTitle(); int qtyOrdered = item.getQtyOrdered(); float price = item.getPrice(); // No check for price and qtyAvailable change // Update the books table and insert an order record sqlStr = "UPDATE books SET qty = qty - " + qtyOrdered + " WHERE id = " + id; //System.out.println(sqlStr); // for debugging stmt.executeUpdate(sqlStr); sqlStr = "INSERT INTO order_records values (" + id + ", " + qtyOrdered + ", '" + custName + "', '" + custEmail + "', '" + custPhone + "')"; //System.out.println(sqlStr); // for debugging stmt.executeUpdate(sqlStr); // Show the book ordered out.println("<tr>"); out.println("<td>" + author + "</td>"); out.println("<td>" + title + "</td>"); out.println("<td>" + price + "</td>"); out.println("<td>" + qtyOrdered + "</td></tr>"); totalPrice += price * qtyOrdered; } out.println("<tr><td colspan='4' align='right'>Total Price: $"); out.printf("%.2f</td></tr>", totalPrice); out.println("</table>"); out.println("<h3>Thank you.</h3>"); out.println("<a href='start'>Back to Search Menu</a>"); out.println("</body></html>"); cart.clear(); // empty the cart } catch (SQLException ex) { cart.clear(); // empty the cart out.println("<h3>Service not available. Please try again later!</h3></body></html>"); Logger.getLogger(CheckoutServlet.class.getName()).log(Level.SEVERE, null, ex); } finally { out.close(); try { if (stmt != null) stmt.close(); if (conn != null) conn.close(); // Return the connection to the pool } catch (SQLException ex) { Logger.getLogger(CheckoutServlet.class.getName()).log(Level.SEVERE, null, ex); } } } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }
Application Deployment Descriptor "web.xml
"
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0"> <servlet> <servlet-name>EntryServlet</servlet-name> <servlet-class>com.xyz.EntryServlet</servlet-class> </servlet> <servlet> <servlet-name>QueryServlet</servlet-name> <servlet-class>com.xyz.QueryServlet</servlet-class> </servlet> <servlet> <servlet-name>CartServlet</servlet-name> <servlet-class>com.xyz.CartServlet</servlet-class> </servlet> <servlet> <servlet-name>CheckoutServlet</servlet-name> <servlet-class>com.xyz.CheckoutServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>EntryServlet</servlet-name> <url-pattern>/start</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>QueryServlet</servlet-name> <url-pattern>/search</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>CartServlet</servlet-name> <url-pattern>/cart</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>CheckoutServlet</servlet-name> <url-pattern>/checkout</url-pattern> </servlet-mapping> <session-config> <session-timeout>30</session-timeout> </session-config> <welcome-file-list> <welcome-file>start</welcome-file> </welcome-file-list> </web-app>
"context.xml
"
<?xml version="1.0" encoding="UTF-8"?> <Context antiJARLocking="true" path="/yaebscart" > <Resource name="jdbc/mysql_ebookshop" auth="Container" type="javax.sql.DataSource" maxActive="100" maxIdle="30" maxWait="10000" removeAbandoned="true" username="myuser" password="xxxx" driverClassName="com.mysql.jdbc.Driver" url="jdbc:mysql://localhost:3306/ebookshop" /> </Context>
With More Input Validation and Abnormal Condition Checks
Again, I rewrite the CartServlet
and CheckoutServlet
to do more input validation and check for abnormal conditions. The codes are messy!
- "
com.xyz.InputFilter
" (as in the above example) - "
CartServlet.java
" [source link] - "
CheckoutServlet.java
" [source link]
User and Role Management
[TODO] Intro
In a practical system, a user has a password; a user may take one or many roles.
User and Role Management via HttpSession
Setup Database
Set up tables users
and user_roles
under the database ebookshop
as follows. Instead of storing plain-text password (which might exposes your password, if you use the same password for all your applications), we create a hash of password, via the MySQL PASSWORD()
function, and store the hash value. The PASSWORD()
function produces a 41-byte hash value. (Read "MySQL Manual: Password Hashing in MySQL").
The following SQL script can be used to setup the database.
use ebookshop; drop table if exists user_roles; drop table if exists users; create table users ( username char(16) not null, password char(41) not null, primary key (username) ); create table user_roles ( username char(16) not null, role varchar(16) not null, primary key (username, role), foreign key (username) references users (username) ); insert into users values ('user1', password('user1')), ('admin1', password('admin1')); insert into user_roles values ('user1', 'user'), ('admin1', 'admin'), ('admin1', 'user'); select * from users; select * from user_roles;
Sequence of Events
- The webapp begins with a login form to prompt the user for username and password, which are submitted to a login script ("
LoginServlet
" with URL "/login
". - The login script verifies the hash of the submitted password with that stored in the database for the username. If the username/password is successful verified, it creates a new
HttpSession
, and places the username and role(s) into the session. This information will be available for all sequence accesses. - All sequence pages retrieve the username and roles from the session, before performing its operations. Otherwise, it shall abort its operations.
- The logout script invalidates the session.
To verify username/password pair, you can issue the following SQL statement. Take note that STRCMP
function is not case-sensitive. It returns 0 if two strings are the same.
// Verify username and password. SELECT * FROM users WHERE STRCMP(username, 'user1') = 0 AND STRCMP(password, PASSWORD('user1') = 0); // Verify username and password, return the roles. SELECT role FROM users, user_roles WHERE STRCMP(users.username, 'user1') = 0 AND STRCMP(users.password, PASSWORD('user1') = 0) AND users.username = user_roles.username;
Let's illustrate with an example with the following sequences:
Login Form "index.html
"
<!DOCTYPE html> <html> <head> <title>Login</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> </head> <body> <h2>Login</h2> <form method="get" action="login"> <table> <tr> <td>Enter your username:</td> <td><input type='text' name='username' /></td> </tr> <tr> <td>Enter your password:</td> <td><input type='password' name='password' /></td> </tr> </table> <br /> <input type="submit" value='LOGIN' /> <input type="reset" value='CLEAR' /> </form> </body> </html>
Take note the the password is sent in clear text in HTTP GET as well as POST request, although it is masked out on the screen with <input type="password">
tag. To use an HTML form to send password, you have to run HTTP with SSL (HTTPS), which encrypts the data transferred.
The Login Script - "LoginServlet.java
" (with URL "/login
")
package com.xyz; import java.io.*; import java.util.*; import java.util.logging.*; import javax.naming.*; import javax.servlet.*; import javax.servlet.http.*; import java.sql.*; import javax.sql.*; public class LoginServlet extends HttpServlet { private DataSource pool; // Database connection pool @Override public void init(ServletConfig config) throws ServletException { try { // Create a JNDI Initial context to be able to lookup the DataSource InitialContext ctx = new InitialContext(); // Lookup the DataSource pool = (DataSource)ctx.lookup("java:comp/env/jdbc/mysql_ebookshop"); if (pool == null) throw new ServletException("Unknown DataSource 'jdbc/mysql_ebookshop'"); } catch (NamingException ex) { Logger.getLogger(LoginServlet.class.getName()).log(Level.SEVERE, null, ex); } } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); Connection conn = null; Statement stmt = null; try { out.println("<html><head><title>Login</title></head><body>"); out.println("<h2>Login</h2>"); conn = pool.getConnection(); // Get a connection from the pool stmt = conn.createStatement(); // Retrieve and process request parameters: username and password String userName = request.getParameter("username"); String password = request.getParameter("password"); boolean hasUserName = userName != null && ((userName = userName.trim()).length() > 0); boolean hasPassword = password != null && ((password = password.trim()).length() > 0); // Validate input request parameters if (!hasUserName) { out.println("<h3>Please Enter Your username!</h3>"); } else if (!hasPassword) { out.println("<h3>Please Enter Your password!</h3>"); } else { // Verify the username/password and retrieve the role(s) StringBuilder sqlStr = new StringBuilder(); sqlStr.append("SELECT role FROM users, user_roles WHERE "); sqlStr.append("STRCMP(users.username, '") .append(userName).append("') = 0 "); sqlStr.append("AND STRCMP(users.password, PASSWORD('") .append(password).append("')) = 0 "); sqlStr.append("AND users.username = user_roles.username"); //System.out.println(sqlStr); // for debugging ResultSet rset = stmt.executeQuery(sqlStr.toString()); // Check if username/password are correct if (!rset.next()) { // empty ResultSet out.println("<h3>Wrong username/password!</h3>"); out.println("<p><a href='index.html'>Back to Login Menu</a></p>"); } else { // Retrieve the roles List<String> roles = new ArrayList<>(); do { roles.add(rset.getString("role")); } while (rset.next()); // Create a new HTTPSession and save the username and roles // First, invalidate the session. if any HttpSession session = request.getSession(false); if (session != null) { session.invalidate(); } session = request.getSession(true); synchronized (session) { session.setAttribute("username", userName); session.setAttribute("roles", roles); } out.println("<p>Hello, " + userName + "!</p>"); out.println("<p><a href='dosomething'>Do Somethings</a></p>"); } } out.println("</body></html>"); } catch (SQLException ex) { out.println("<h3>Service not available. Try again later!</h3></body></html>"); Logger.getLogger(LoginServlet.class.getName()).log(Level.SEVERE, null, ex); } finally { out.close(); try { if (stmt != null) stmt.close(); if (conn != null) conn.close(); // Return the connection to the pool } catch (SQLException ex) { Logger.getLogger(LoginServlet.class.getName()).log(Level.SEVERE, null, ex); } } } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }
Inside the Login Session - "DoSomethingServlet.java
" (URL "/dosomething
")
package com.xyz;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import java.util.*;
public class DoSomethingServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
try {
out.println("<html><head><title>Do Something</title></head><body>");
out.println("<h2>Do Somethings...</h2>");
// Retrieve and Display the username and roles
String userName;
List<String> roles;
HttpSession session = request.getSession(false);
if (session == null) {
out.println("<h3>You have not login!</h3>");
} else {
synchronized (session) {
userName = (String) session.getAttribute("username");
roles = (List<String>) session.getAttribute("roles");
}
out.println("<table>");
out.println("<tr>");
out.println("<td>Username:</td>");
out.println("<td>" + userName + "</td></tr>");
out.println("<tr>");
out.println("<td>Roles:</td>");
out.println("<td>");
for (String role : roles) {
out.println(role + " ");
}
out.println("</td></tr>");
out.println("<tr>");
out.println("</table>");
out.println("<p><a href='logout'>Logout</a></p>");
}
out.println("</body></html>");
} finally {
out.close();
}
}
}
The Logout Script - "LogoutServlet.java
" (URL "/logout
")
package com.xyz; import java.io.*; import javax.servlet.*; import javax.servlet.http.*; public class LogoutServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); try { out.println("<html><head><title>Logout</title></head><body>"); out.println("<h2>Logout</h2>"); HttpSession session = request.getSession(false); if (session == null) { out.println("<h3>You have not login!</h3>"); } else { session.invalidate(); out.println("<p>Bye!</p>"); out.println("<p><a href='index.html'>Login</a></p>"); } out.println("</body></html>"); } finally { out.close(); } } }
Web Application Deployment Descriptor "WEB-INF\web.xml
"
<?xml version="1.0" encoding="UTF-8"?> <web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"> <servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.xyz.LoginServlet</servlet-class> </servlet> <servlet> <servlet-name>DoSomethingServlet</servlet-name> <servlet-class>com.xyz.DoSomethingServlet</servlet-class> </servlet> <servlet> <servlet-name>LogoutServlet</servlet-name> <servlet-class>com.xyz.LogoutServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>DoSomethingServlet</servlet-name> <url-pattern>/dosomething</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>LogoutServlet</servlet-name> <url-pattern>/logout</url-pattern> </servlet-mapping> <session-config> <session-timeout>30</session-timeout> </session-config> <welcome-file-list> <welcome-file>index.html</welcome-file> </welcome-file-list> </web-app>
"META-INF\context.xml
"
<?xml version="1.0" encoding="UTF-8"?> <Context antiJARLocking="true" path="/usertest" > <Resource name="jdbc/mysql_ebookshop" auth="Container" type="javax.sql.DataSource" maxActive="100" maxIdle="30" maxWait="10000" removeAbandoned="true" username="myuser" password="xxxx" driverClassName="com.mysql.jdbc.Driver" url="jdbc:mysql://localhost:3306/ebookshop" /> </Context>
User and Role Management via Container
[TODO] HttpServletRequest
's authenticate()
, login()
and logout()
.
Input Validation
Observed that many lines of codes are used in validating the inputs provided by the client in the servlet (server-side program). You can perform input validation on the server-side as well as on the client-side.
Client-side Input Validation using JavaScript
We could perform client-side input validation using JavaScript. Validating user inputs with JavaScript before the data leaves the browser provides a much faster response, but it doesn't necessarily eliminate the checks you have to do on the server side. This is because a user might have disabled JavaScript or maliciously issue URLs that bypasses the client-side checks.
Read "JavaScript Examples - Validating Form Inputs".
Input Validation via JavaServer Faces (JSF)
[TODO]
Uploading Files (Servlet 3.0)
Before Servlet 3.0, processing file upload requires 3rd party libraries such as Apache Commons FileUpload. Servlet 3.0 (which requires Tomcat 7) builds in file upload support.
File upload over HTTP is specified in "RFC 1867 Form-based File Upload in HTML". Read "File Upload using multipart/form-data POST Request".
Client-side HTML Form: "FileUpload.html
"
On the client-side, you provides an HTML <form>
with an input element <input type="file">
as follows:
<!DOCTYPE html> <html> <head> <title></title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> </head> <body> <h2>Upload File</h2> <form method="post" enctype="multipart/form-data" action="upload"> Choose a file: <input type="file" name="uploadedFile" /><br /> <input type="submit" /> </form> </body> </html>
Server-side
Servlet 3.0 introduces a new annotation @MultipartConfig
with these attributes:
location
: An absolute path to a directory in your file system (NOT relative to your context root) to store files temporarily while processing parts, when the file is bigger thanfileSizeThreshold
. This directory shall exist; otherwise, anIOException
will be thrown.fileSizeThreshold
: The file size in bytes after which the file will be stored temporarily in thelocation
.maxFileSize
: The maximum file size in bytes. If the size of the file is bigger than this, Tomcat throws anIllegalStateException
.maxRequestSize
: The maximum size in bytes for the entiremultipart/form-data
request (i.e., all the parts).
For example,
@MultipartConfig(location="d:\\temp\\upload", fileSizeThreshold=1024*1024, maxFileSize=5*1024*1024, maxRequestSize=2*5*1024*1024)
A new interface javax.servlet.http.Part
is also introduced to represent a part of a form item that were received within a multipart/form-data
POST request. The following methods are declared:
getName()
: Get the name of this part.getSize()
: Get the size of this part.getInputStream()
: Get the content of this part as anInputStream
.write(String filename)
: Write this part to file. The filename is relative to the "location
" in@MultipartConfig
. The container may simply rename the temporary file. Existing file will be overridden.delete()
: Delete the underlying storage, including the temporary file. Do not calldelete()
afterwrite()
.getContentType()
: Get the content type of this part.getHeader(String name)
,getHeaders(String name)
,getHeaderNames()
: Get the header.
The method request.getParts()
(in javax.servlet.http.HttpServletRequest
) returns a collection of all parts. The request.getPart(String name)
returns a Part
for given name attribute (if you have other input elements besides file).
"FileUploadServlet30.java
" with URL "/upload
"
package com.xyz; import java.io.*; import java.util.Collection; import javax.servlet.*; import javax.servlet.annotation.*; import javax.servlet.http.*; @WebServlet( name = "upload", urlPatterns = {"/upload"}) @MultipartConfig( location="d:\\temp\\upload", fileSizeThreshold=1024*1024, maxFileSize=5*1024*1024, maxRequestSize=2*5*1024*1024) public class FileUploadServlet30 extends HttpServlet { @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html"); PrintWriter out = response.getWriter(); Collection<Part> parts = request.getParts(); try { out.write("<h2>Number of parts : " + parts.size() + "</h2>"); for(Part part : parts) { printPartInfo(part, out); String filename = getFileName(part); if (filename != null) { part.write(filename); // relative to location in @MultipartConfig } } } finally { out.close(); } } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { getServletContext() .getRequestDispatcher("/FileUpload.html") .forward(request, response); } // Print the headers for the given Part private void printPartInfo(Part part, PrintWriter writer) { StringBuilder sb = new StringBuilder(); sb.append("<p>Name: ").append(part.getName()).append("<br>"); sb.append("ContentType: ").append(part.getContentType()).append("<br>"); sb.append("Size: ").append(part.getSize()).append("<br>"); for(String header : part.getHeaderNames()) { sb.append(header).append(": ").append(part.getHeader(header)).append("<br>"); } sb.append("</p>"); writer.write(sb.toString()); } // Gets the file name from the "content-disposition" header private String getFileName(Part part) { for (String token : part.getHeader("content-disposition").split(";")) { if (token.trim().startsWith("filename")) { return token.substring(token.indexOf('=') + 1).trim() .replace("\"", ""); } } return null; } }
Total parts : 1 Name: uploadedFile ContentType: text/plain Size: 811 content-type: text/plain content-disposition: form-data; name="uploadedFile"; filename="uploadedFile.txt"
- The client-side "
FileUpload.html
" has one submission part, i.e.,<input type="file">
. - The
printPartInfo()
prints the headers of the given part. The output is as shown above. - The
part.write()
method is used to write the file under thelocation
of the@MultipartConfig
with the filename extracted from thecontent-disposition
header.
An Multi-part HTML Form - "FileUploadMultipart.html
"
The HTML form below has four input fields, which will be sent in four parts, as shown in the output below.
<!DOCTYPE html> <html> <head> <title></title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> </head> <body> <h2>Upload File</h2> <form method="post" enctype="multipart/form-data" action="upload"> Who are you: <input type="text" name="username" /><br /> Choose the file to upload: <input type="file" name="file1" /><br /> Choose another file to upload: <input type="file" name="file2" /><br /> Comments:<br /> <textarea name="comment"></textarea><br /> <input type="submit" value="SEND" /> </form> </body> </html>
Number of parts : 4 Name: username ContentType: null Size: 5 content-disposition: form-data; name="username" Name: file1 ContentType: text/plain Size: 811 content-type: text/plain content-disposition: form-data; name="file1"; filename="uploadedFile.txt" Name: file2 ContentType: audio/mpeg Size: 4842585 content-type: audio/mpeg content-disposition: form-data; name="file2"; filename="away.mp3" Name: comment ContentType: null Size: 7 content-disposition: form-data; name="comment"
Notes:
- You can use
request.getPart(name)
to retrieve a particular part with the given name attribute, instead ofrequest.getParts()
, which retrieves all parts in aCollection<Part>
. - You can also use the following code to read the data from each part:
InputStream inStream = request.getPart(part.getName()).getInputStream(); int byteRead; while ((byteRead = inStream.read()) != -1) { out.write(byteRead); }
Deploying a Web Application in a WAR file
To deploy a Java webapp, you "zip" all the files and resources together in a single WAR (Web Application Archive) file. A WAR file, like a JAR file, uses ZIP algorithm, and can be opened using WinZIP or WinRAR.
You could use the JDK's jar
utility to produce a WAR file as follows.
.... Change current directory to the webapp's context root
> jar cvf test.war .
(NetBeans) A war file is generated when you build the project, under the project's "dist
" directory.
To deploy a WAR file, simply drop the WAR file (says test.war
) into $CATALINA_HOME\webapps
. A context called "test
" will be created automatically. You can access the web application via URL http://host:port/test
.
Tomcat will unpack the test.war
into a "test
" directory in $CATALINA_HOME\webapps
, if the configuration option unpackWARs="true"
. Otherwise, it will run directly from the WAR file without unpacking, which may involve some overhead. You may need to remove the unpacked directory, if you drop a new version.
A Secured Payment Gateway
[TODO]
REFERENCES & RESOURCES
- TODO