Monday, May 09, 2005

0 to Struts in 60 minutes - Part II

Our goal :

First Iteration :
* set up a login page, login formbean, login process Actionservlet stub

Second Iteration :
* set up log4j for logging purposes
* use a pattern - Data Access Object pattern
* talk to mysql database
* logic for authentication in login actionservlet

First Iteration :

Setting up login page, login process servlet stub, welcome page

Lets build this project iteratively. First the skeleton and then slowly we will start adding sinews adding more complexity. A simple login page that submits the values to the login servlet. ( Well it has to go through the controller servlet - we will see that in a minute ) - and the login process servlet right now doesnt do anything big - just accepts the values and displays the username being entered. It still does not have the ability to talk to database - it has to wait !!

We need to create 4 files ( 2 JSP,2 Java) and tamper with struts_config.xml

Login.jsp


<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>

<html>

<body>

<html:form action="/actions/actionlogin">

Username : <html:text property="username"/><BR>

Password : <html:password property="password"/>

<html:submit>Submit</html:submit>

</html:form>

</body>

</html>





We are taking advantage of struts tags. The html code we have to write gets minimised a lot when we use struts tags - things like populating a select drop down menu and selecting default value can all be written in a single line - makes the html code look neat and maintenance is easy. Perhaps in the next blog entry I will write a page takes advantage of all the html tags, and other bells and whistles in struts. Save this file as login.jsp and let it sit in under webapps/antiPC/pages

The login ActionServlet stub


package net.kvrlogs.antiPC.actions;



import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import org.apache.struts.action.*;



public class Login extends Action {

public ActionForward execute(ActionMapping mapping, ActionForm form,

HttpServletRequest request, HttpServletResponse response)

throws Exception {



return mapping.findForward("success");

}

}




Save this under WEB-INF/src/net/kvrlogs/antiPC/actions as Login.java

If you notice this servlet returns a value called "success". This is picked up by the controller servlet and based on the value it calls up the appropriate jsp page or another servlet.

We are slowly starting to get into struts.
How is this servlet (its called ActionServlet) different from a simple servlet.

1. We extend Action - instead of HTTPServlet.
2. We do not override doGet or doPost anymore. Instead we override a method called execute.
3. execute is given 4 important objects
mapping
form
request - same as a servlet
response - same as a servlet

What is mapping?

Mapping is the hotline to the controller servlet. This login servlet cannot ( and should not ) direct control to another JSP page. It it does like that then it blows away the main concept of MVC Architecture.

This mapping is done in struts-config.xml file from where controller servlet reads and gets its intelligence.

What is form?

Form is the ActionForm. Whenever a html form is submitted all the field values are put inside a JavaBean called FormBean. For now just type the following code and save it under /WEB-INF/src/net/kvrlogs/antiPC/forms/LoginFormBean.java

If you are using eclipse there is a short cut to generate this file :
Just type the following
private String username;
private String password;

Then eclipse can automatically generate getters and setters for the variables like this gettersetter

Point to note is the variable names username and password should be same as the html field names given in login.jsp page. When the login.jsp page is submitted, the values in username and password are put in this formbean and sent to the login servlet. This is done by Java reflection where it automatically discovers the right variable names.

Also note that the getters and setters follow javabean naming rules ( if you let eclipse do its job you will not be mistyping the getter setter names and would not later haunt you when the form variables are not seen in the login servlet ).

*the getter, setter should be named as get/set and First letter of variable capitalized.

if variable name is username
getter will be getUsername

* the variable should be private, whereas the getter setter should be public.


package net.kvrlogs.antiPC.forms;



import org.apache.struts.action.ActionForm;



public class LoginFormBean extends ActionForm {

private String username;

private String password;



public String getPassword() {

return password;

}

public void setPassword(String password) {

this.password = password;

}

public String getUsername() {

return username;

}

public void setUsername(String username) {

this.username = username;

}

}





In struts-config.xml
Go to the node called


<action

path="/Welcome"

forward="/pages/Welcome.jsp"/>




Change the above to this


<action

path="/Welcome"

forward="/pages/login.jsp"/>





Now if you open the index.jsp page there is a redirect command

<logic:redirect forward="welcome"/>


When index.jsp page is typed in the browser, when the page loads it sees the redirect to "welcome" action. The controller servlet picks this up - finds the action mapping in struts-config.xml as directing to forward to /pages/login.jsp - and thats what the browser will see - login.jsp

Next we have to define the action for /actions/actionlogin

<action

path="/actions/actionlogin"

type="net.kvrlogs.antiPC.actions.Login"

parameter="/pages/index.jsp">

<forward

name="success"

path="/pages/homepage.jsp"/>

</action>





path : Its the path we type in the form action
type : which actionservlet is called to execute this action
parameter : from where this request is coming from

forward : After the actionservlet does it job it sends a message - "success" in our case. Heres where we decide where the "success" should take us to - to /pages/homepage.jsp

Lets create homepage.jsp


<html>

<body>

You are in

</body>

</html>


Save this in pages/homepage.jsp
Now this is what we have done so far in our first iteration.

*Created login.jsp, Login.java, homepage.jsp
* Mapped the logic flow in struts-config.xml under the node

Time to build, and try it out in your browser.

Save everything.
Click on the directiory WEB-INF/src ( or where the build.xml file is )
Select the little run button from eclipse and choose Run as ant build.
Pray :)

I got a build error saying this :

[javac] /usr/local/tomcat/webapps/antiPC/WEB-INF/src/net/kvrlogs/antiPC/actions/Login.java:3: package javax.servlet.http does not exist

I added this to the pathelement in build.xml

<pathelement path ="/Library/Tomcat/common/lib/servlet-api.jar"/>

Once it builds, restart antiPC in tomcat manager. And type http://localhost:8080/antiPC in webbrowser.

You will see this
login

Hit submit and you will see this youarein

Second Iteration :

* set up log4j for logging purposes
* use a pattern - Data Access Object pattern
* talk to mysql database
* logic for authentication in login actionservlet


Setting up log4j

log4j - is a replacement for system.out.printlns developers write for debugging. Production level code should not spit out these. So these printlns have to be hunted and commented out,source code compiled - and also has to be uncommented later in maintenance mode when something breaks - its a pain.

Log4j allows us to set logging on or off globally - so when code goes to production, all we have to do is set the logging level we need by editing a configuration file. Also there are lots of other advantages

* granularity can be controlled - debug,warn,info,error,fatal,log
* output can be anything - sysout, file etc.
* since its only job is to log it does an efficient job of it

Get log4j from good old apache.
http://logging.apache.org/log4j/docs/download.html

Under dist directory there is the jar file : log4j-1.2.9.jar

Copy it to TOMCAT_HOME/common/lib directory and also in antiPC/WEB-INF/lib.

Inside TOMCAT_HOME/common/classes, create a file called log4j.properties and type these into it.

log4j.rootLogger=info, R
log4j.appender.R=org.apache.log4j.ConsoleAppender
log4j.appender.R.layout=org.apache.log4j.PatternLayout
log4j.appender.R.layout.ConversionPattern=%-5p %-30.30c{1} %x - %m%n

( I have no clue what all these appenders and layouts are - I just got it from log4j website. But it works :) )

There are lot of steps involved before we can talk to the database. Heres what we are trying to accomplish.

When the server loads the application it will load a listener defined in WEB.XML file under the node . When the container initializes this class is called.

Now the listener reads the entries in WEB.XML for database driver name,database name,database username,password and created a DBHandler object - which is kind of a helper class that accepts sql statements and executes them for you. The listener puts this DBHandler object as an attribute in context. Think of context variables as a global variable for the entire application. So whenever any servlet when it wants to talk to the database it can get the DBHandler object from the context and use it.

Advantages of this long winded approach?

* Database connection code sits at one place
* Database details are stored in web.xml - so application need not be recompiled when any of the values change. Just a reload is sufficient.

So to start the long winded road- lets first enter the database related stuff in web.xml


<context-param>

<param-name>dbDriver</param-name>

<param-value>org.gjt.mm.mysql.Driver</param-value>

</context-param>

<context-param>

<param-name>dbDatabase</param-name>

<param-value>jdbc:mysql://localhost/antiPC</param-value>

</context-param>

<context-param>

<param-name>dbUsername</param-name>

<param-value>yourusername</param-value>

</context-param>

<context-param>

<param-name>dbPassword</param-name>

<param-value>yourpassword</param-value>

</context-param>



<listener>

<listener-class>

net.kvrlogs.antiPC.init.MyServletContextListener

</listener-class>

</listener>




Next save DBHandler.java under /WEB-iNF/src/net/kvrlogs/antipc/utils/DBHandler.java




package net.kvrlogs.antiPC.utils;



import java.sql.*;

import org.apache.log4j.*;

import java.util.*;



public class DBHandler {



static Logger _logger = Logger.getLogger(DBHandler.class.getName());



String _driverName = null;



String _databaseUrl = null;



Connection _dbConn = null;



PreparedStatement _pstmt = null;



String _queryString = null;



public DBHandler(String driverName, String databaseUrl, String _username,

String _password) {



String methodsig = "DBHandler.DBHandler()";



_logger.info(driverName + "," + databaseUrl + ","

+ _username + "," + _password);



//Verify that the driver class exists



try {



Class.forName(driverName);



} catch (Exception e) {



e.printStackTrace();



_logger.error("Driver could not be found: " + e);



return;



}



//Get a connection



try {



_dbConn = DriverManager.getConnection(databaseUrl, _username,

_password);



_driverName = driverName;



_databaseUrl = databaseUrl;



} catch (SQLException sqle) {



_logger.error( "Couldn't connect to database. "

+ sqle);



return;



}



//Log it



_logger.info("Connected to " + _databaseUrl);



}



public boolean setQueryString(String queryString) {



String methodsig = "DBHandler.setQueryString()";



if (_dbConn == null) {



_logger.error("Error!!! - Couldn't set the query string because the Connection was null. The DBHandler was not constructed correctly or the database does not exist.");



return false;



}



try {



_pstmt = _dbConn.prepareStatement(queryString);



_queryString = queryString;



} catch (SQLException sqle) {



_logger.error("Couldn't create PreparedStatement. " + sqle);



return false;



}



return true;



}



public void close() {



try {



_dbConn.close();



} catch (SQLException sqle) {



}



}



/**

*

* Do a select query on a prepared statement with no arguments

*

*/



public ResultSet lookup() {



String methodsig = "DBHandler.lookup(String)";



_logger.debug("Looking up query: '" + _queryString

+ "'");



try {



if (!_pstmt.execute()) {



_logger.debug("The resultset was empty on the lookup..");



}



return _pstmt.getResultSet();



} catch (SQLException sqle) {



_logger.debug("Couldn't connect to database, or create PreparedStatement. "

+ sqle);



return null;



}



}



/**

*

* Do a select query on a prepared statement with single argument,

*

* where the argument is a String

*

*/



public ResultSet lookup(String arg) {



String methodsig = "DBHandler.lookup(String)";



_logger.debug("Looking up: '" + arg + "' with query '"

+ _queryString + "'");



try {



_pstmt.setString(1, arg);



if (!_pstmt.execute()) {



_logger.debug("The resultset was empty on the lookup..");



}



return _pstmt.getResultSet();



} catch (SQLException sqle) {



_logger.debug("Couldn't connect to database, or create PreparedStatement. "

+ sqle);



return null;



}



}



/**

*

* Do a select query on a prepared statement with single argument,

*

* where the argument is an integer

*

*/



public ResultSet lookup(int arg) {



final String methodsig = "DBHandler.lookup(int)";



ResultSet returnval = lookup(Integer.toString(arg));



return returnval;



}



/**

*

* Do a select query on a prepared statement with a vector of arguments,

*

*/



public ResultSet lookup(Vector v) {



final String methodsig = "DBHandler.lookup(Vector)";



try {



//i is the a counter that places each variable in the vector into

// the



//proper place in the prepared statement



int i = 1;



for (Enumeration enum = v.elements(); enum.hasMoreElements();) {



_pstmt.setString(i++, (String) enum.nextElement());



}



if (!_pstmt.execute()) {



_logger.debug("The resultset was empty on the lookup..");



}



return _pstmt.getResultSet();



} catch (SQLException sqle) {



_logger.debug("Couldn't connect to database, or create PreparedStatement. "

+ sqle);

return null;



}



}



/**

*

* Insert a row of values (Vector of args) into a table

*

*/



public int insert(Vector args) {



String methodsig = "DBHandler.insert()";



//Create a string out of the args so we can pass it into the logfile

// message



String argString = new String("(");



for (Enumeration enum = args.elements(); enum.hasMoreElements();) {



Object o = enum.nextElement();



if (o instanceof Integer) {



argString = argString.concat((Integer) o + ",");



}



else {



argString = argString.concat((String) o + ",");



}



}



argString = argString.substring(0, argString.length() - 1);



argString = argString.concat(")");



_logger.debug("Inserting: '" + argString

+ "' with query '" + _queryString + "'");



//Create the prepared statement by passing in each argument of the

// vector



try {



for (int i = 0; i < args.size(); i++) {



Object o = args.elementAt(i);



// if (o == null) {



// _pstmt.setNull(i+1, 1);



// }



if (o instanceof Integer) {



_pstmt.setInt(i + 1, ((Integer) o).intValue());



}



else {



_pstmt.setString(i + 1, (String) o);



}



}



int updateResult = _pstmt.executeUpdate();



_logger.debug("The insert updated " + updateResult

+ " rows");



return updateResult;



} catch (SQLException sqle) {



_logger.debug("Couldn't connect to database, or create PreparedStatement. "

+ sqle);



//sqle.printStackTrace();

return 0;



}



}



/**

*

* Execute a query with no args

*

*/



public boolean execute() {



String methodsig = "DBHandler.execute()";



_logger.debug("Executing query: '" + _queryString

+ "'");



try {



boolean returnval = _pstmt.execute();





return returnval;



} catch (SQLException sqle) {



_logger.debug("Couldn't connect to database. "

+ sqle);



return false;



}



}



/**

*

* Execute an update query with a String as an arg

*

*/



public int executeUpdate(String arg) {



String methodsig = "DBHandler.executeUpdate(String)";



_logger.debug("Updating '" + arg

+ "' with query string '" + _queryString + "'");



try {



_pstmt.setString(1, arg);



int updateResult = _pstmt.executeUpdate();



_logger.debug("The updated affected "

+ updateResult + " rows");





return updateResult;



} catch (SQLException sqle) {



_logger.debug("Couldn't connect to database. "

+ sqle);



return 0;



}



}



/**

*

* Execute an update query with a Vector of Strings as args

*

*/



public int executeUpdate(Vector args) {



String methodsig = "DBHandler.executeUpdate(Vector)";





//Create a string out of the args so we can pass it into the logfile

// message



String argString = new String("(");



for (Enumeration enum = args.elements(); enum.hasMoreElements();) {



Object o = enum.nextElement();



if (o instanceof Integer) {



argString = argString.concat((Integer) o + ",");



}



else {



argString = argString.concat((String) o + ",");



}



}



argString = argString.substring(0, argString.length() - 1);



argString = argString.concat(")");



_logger.debug("Executing: '" + argString

+ "' with query '" + _queryString + "'");



//Create the prepared statement by passing in each argument of the

// vector



try {



for (int i = 0; i < args.size(); i++) {



Object o = args.elementAt(i);



if (o instanceof Integer) {



_pstmt.setInt(i + 1, ((Integer) o).intValue());



}



else {



_pstmt.setString(i + 1, (String) o);



}



}



int executeResult = _pstmt.executeUpdate();



_logger.debug("The execute updated returned '"

+ executeResult + "'");

return executeResult;



} catch (SQLException sqle) {



_logger.debug("Couldn't connect to database, or create PreparedStatement. "

+ sqle);



//sqle.printStackTrace();

return 0;



}



}



public String getDatabaseUrl() {



return _databaseUrl;



}



public String getDriverName() {



return _driverName;



}



public String getQueryString() {



return _queryString;



}

}



Next we define the listener.

Save the following code into WEB-INF/src/net.kvrlogs.antiPC.init as MyServletContextListener


package net.kvrlogs.antiPC.init;

import javax.servlet.*;

import net.kvrlogs.antiPC.utils.DBHandler;



public class MyServletContextListener implements ServletContextListener{

public void contextInitialized(ServletContextEvent event){

ServletContext cx = event.getServletContext();

String _databaseUrl = cx.getInitParameter("dbDatabase");

String _driverName = cx.getInitParameter("dbDriver");

String _user = cx.getInitParameter("dbUsername");

String _password = cx.getInitParameter("dbPassword");

DBHandler dbHandler = new DBHandler(_driverName,_databaseUrl,_user,_password);

cx.setAttribute("dbhandle",dbHandler);



}



public void contextDestroyed (ServletContextEvent event){

}

}


For now I am skipping writing DAO pattern - this post is getting long. Will just go ahead and put the dbhandler calls inside login action servlet itself.

Now add the following lines into Login.java


ServletContext context = getServlet().getServletContext();

DBHandler db = (DBHandler)context.getAttribute("dbhandle");

LoginFormBean loginBean = (LoginFormBean) form;

String username = loginBean.getUsername();

String password = loginBean.getPassword();

String sql = "SELECT * FROM antiPC_users where user_name = '" + username + "'" +

" and user_password = '" + password +"'" ;

db.setQueryString(sql);

ResultSet rs = db.lookup();

if(rs != null && rs.next())

return mapping.findForward("success");

else

return mapping.findForward("failure");


Now I have added a new mapping called failure. Lets define it in struts-config.xml and we are all set.

Add this below or above success mapping.

<forward

name="failure"

path="/pages/login.jsp"/>


Save all. Build. Stop antiPC. Start antiPC. Go to browser. Enter tester,tester ( or whatever value you have given in user table ) - see if it goes to You are in page. Enter wrong values - see if it takes you back to login page again.

You are now free to move about struts.ting.



ps: If you need the war file email me to kvrlogs@gmail.com