Tuesday, March 20, 2007

User session filter

Capture the User

By default a HTTP session will expire after 30 minutes of inactivity. Although you can change this by the following entry in the web.xml, where the timeout can be set in minutes:

<session-config>
    <session-timeout>30</session-timeout>
</session-config>

In most of the web form based authentication solutions it is designed so that when an user logs in, it's User object will be looked up from the database and put as an attribute in the HTTP session using HttpSession#setAttribute(). When the session expires or has been invalidated (when the user closes and reopens the browser window, for example), then the User object will be garbaged and become unavailable. So the user have to login everytime when he visits the website using the same PC after a relatively short period of inactivity or when he opens a new browser session. This can be annoying after times if the user visits the website (or forum) very frequently.

At some websites you can see an option "Remember me on this computer" at the login form. Such websites put an unique ID in a long-living cookie, and uses this ID to lookup the usersession and the eventual logged in user in the database. This makes the login totally independent of the lifetime of the HTTP session, so that the user can decide himself when to login and logout. Checking for the logged in user in a new session can be done easily using a Filter. This article shows an example of such a Filter.

Back to top

Prepare DTO's

First prepare the DTO's (Data Transfer Objects) for UserSession and User which can be used to hold information about the usersession and the user. You can map those DTO's to the database.

package mymodel;

import java.util.Date;

public class UserSession {

    // Properties ---------------------------------------------------------------------------------

    private String cookieId;
    private User user;
    private Date creationDate;
    private Date lastVisit;
    private int hits;

    // Constructors -------------------------------------------------------------------------------

    /**
     * Default constructor. 
     */
    public UserSession() {
        // Keep it alive.
    }

    /**
     * Construct new usersession with given cookie ID. 
     */
    public UserSession(String cookieId) {
        this.cookieId = cookieId;
        this.creationDate = new Date();
        this.lastVisit = new Date();
    }

    // Getters and setters ------------------------------------------------------------------------

    // Implement default getters and setters here the usual way.

    // Helpers ------------------------------------------------------------------------------------

    /**
     * Add hit (pageview) to the UserSession. Not necessary, but nice for stats.
     */
    public void addHit() {
        this.hits++;
        this.lastVisit = new Date();
    }

    /**
     * A convenience method to check if User is logged in.
     */
    public boolean isLoggedIn() {
        return user != null;
    }

}
package mymodel;

public class User {

    // Properties ---------------------------------------------------------------------------------

    private Long id;
    private String username;
    private String password;
    // Implement other properties here, depending on the requirements.
    // For example: email address, firstname, lastname, homepage, etc.

    // Getters and setters ------------------------------------------------------------------------

    // Implement default getters and setters here the usual way.

}
Back to top

UserSessionFilter

Here is how a UserSessionFilter should look like.

Note: A DAO example can be found here.

package mycontroller;

import java.io.IOException;
import java.util.UUID;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import mydao.DAOException;
import mydao.DAOFactory;
import mydao.UserSessionDAO;
import mymodel.UserSession;
import mymodel.User;

/**
 * The UserSession filter.
 * @author BalusC
 * @link http://balusc.blogspot.com/2007/03/user-session-filter.html
 */
public class UserSessionFilter implements Filter {

    // Constants ----------------------------------------------------------------------------------

    private static final String MANAGED_BEAN_NAME = "userSession";
    private static final String COOKIE_NAME = "UserSessionFilter.cookieId";
    private static final int COOKIE_MAX_AGE = 31536000; // 60*60*24*365 seconds; 1 year.

    // Vars ---------------------------------------------------------------------------------------

    private UserSessionDAO userSessionDAO;

    // Actions ------------------------------------------------------------------------------------

    /**
     * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
     */
    public void init(FilterConfig filterConfig) {
        // Just do your DAO thing. You can make the databaseName a context init-param as well.
        // Also see the DAO tutorial.
        DAOFactory daoFactory = DAOFactory.getInstance("databaseName");
        userSessionDAO = daoFactory.getUserSessionDAO();
    }

    /**
     * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, 
     *      javax.servlet.ServletResponse, javax.servlet.FilterChain)
     */
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException
    {
        // Check PathInfo.
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String pathInfo = httpRequest.getRequestURI().substring(httpRequest.getContextPath().length());
        if (pathInfo.startsWith("/inc")) {
            // This is not necessary, but it might be useful if you want to skip include
            // files for example. You can put include files (subviews, images, css, js)
            // in one folder, called "/inc". If those include files are loaded, then
            // continue the filter chain and abort this filter, because it is usually not
            // necessary to lookup for any UserSession then. Or, if the url-pattern in the
            // web.xml is specific enough, then this if-block can just be removed.
            chain.doFilter(request, response);
            return;
        }

        // Get UserSession from HttpSession.
        HttpSession httpSession = httpRequest.getSession();
        UserSession userSession = (UserSession) httpSession.getAttribute(MANAGED_BEAN_NAME);

        if (userSession == null) {

            // No UserSession found in HttpSession; lookup ID in cookie.
            String cookieId = getCookieValue(httpRequest, COOKIE_NAME);

            if (cookieId != null) {

                // ID found in cookie. Lookup UserSession by cookie ID in database.
                // Do your "SELECT * FROM UserSession WHERE CookieID" thing.
                try {
                    userSession = userSessionDAO.find(cookieId); 
                    // This can be null. If this is null, then the session is deleted
                    // from DB meanwhile or the cookie is just fake (hackers!).
                } catch (DAOException e) {
                    // Do your exception handling thing.
                    setErrorMessage("Loading UserSession failed.", e);
                }
            }

            if (userSession == null) {

                // No ID found in cookie, or no UserSession found in DB.
                // Create new UserSession.
                // Do your "INSERT INTO UserSession VALUES values" thing.
                cookieId = UUID.randomUUID().toString();
                userSession = new UserSession(cookieId);
                try {
                    userSessionDAO.save(userSession);
                } catch (DAOException e) {
                    // Do your exception handling thing.
                    setErrorMessage("Creating UserSession failed.", e);
                }

                // Put ID in cookie.
                HttpServletResponse httpResponse = (HttpServletResponse) response;
                setCookieValue(httpResponse, COOKIE_NAME, cookieId, COOKIE_MAX_AGE);
            }

            // Set UserSession in current HttpSession.
            httpSession.setAttribute(MANAGED_BEAN_NAME, userSession);
        }

        // Add hit and update UserSession.
        // Do your "UPDATE UserSession SET values WHERE CookieID" thing.
        userSession.addHit();
        try {
            userSessionDAO.save(userSession);
        } catch (DAOException e) {
            // UserSession might be deleted from DB meanwhile.
            // Reset current UserSession and re-filter.
            httpSession.setAttribute(MANAGED_BEAN_NAME, null);
            doFilter(request, response, chain);
            return;
        }

        // Continue filtering.
        chain.doFilter(request, response);
    }

    /**
     * @see javax.servlet.Filter#destroy()
     */
    public void destroy() {
        // Apparently there's nothing to destroy?
    }

    // Helpers (may be refactored to some utility class) ------------------------------------------

    /**
     * Retrieve the cookie value from the given servlet request based on the given
     * cookie name.
     * @param request The HttpServletRequest to be used.
     * @param name The cookie name to retrieve the value for.
     * @return The cookie value associated with the given cookie name.
     */

    public static String getCookieValue(HttpServletRequest request, String name) {
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (cookie != null && name.equals(cookie.getName())) {
                    return cookie.getValue();
                }
            }
        }
        return null;
    }

    /**
     * Set the cookie value in the given servlet response based on the given cookie
     * name and expiration interval.
     * @param response The HttpServletResponse to be used.
     * @param name The cookie name to associate the cookie value with.
     * @param value The actual cookie value to be set in the given servlet response.
     * @param maxAge The expiration interval in seconds. If this is set to 0,
     * then the cookie will immediately expire.
     */
    public static void setCookieValue(
        HttpServletResponse response, String name, String value, int maxAge)
    {
        Cookie cookie = new Cookie(name, value);
        cookie.setMaxAge(maxAge);
        response.addCookie(cookie);
    }

}

You can activate the UserSessionFilter by adding the following lines to the web.xml. Take note: the filters are executed in the order as they are definied in the web.xml.

<filter>
    <filter-name>userSessionFilter</filter-name>
    <filter-class>mycontroller.UserSessionFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>userSessionFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
Back to top

Login and Logout

Now, with such an UserSessionFilter the user can decide himself when to login and logout. Here is an example how you can let the user login and logout. It is just all about putting and removing the User object from the UserSession object.

The basic JSF code for the login:

<h:form>
    <h:panelGrid columns="2">
        <h:outputText value="Username" />
        <h:inputText value="#{userForm.username}" />

        <h:outputText value="Password" />
        <h:inputSecret value="#{userForm.password}" />

        <h:panelGroup />
        <h:commandButton value="login" action="#{userForm.login}" />
    </h:panelGrid>
</h:form>

And the basic JSF code for the logout:

<h:form>
    <h:commandButton value="logout" action="#{userForm.logout}" />
</h:form>

Finally the backing bean's code. Note: MathUtil#hashMD5() is described here. A DAO example can be found here.

package mycontroller;

import javax.faces.context.FacesContext;
import javax.servlet.http.HttpSession;

import mydao.DAOException;
import mydao.DAOFactory;
import mydao.UserDAO;
import mydao.UserSessionDAO;
import mymodel.UserSession;
import mymodel.User;

public class UserForm {

    // Vars ---------------------------------------------------------------------------------------

    // Just do your DAO thing. You can make the databaseName a context init-param as well.
    // Also see the DAO tutorial.
    private static DAOFactory daoFactory = DAOFactory.getInstance("databaseName");
    private static UserDAO userDAO = daoFactory.getUserDAO();
    private static UserSessionDAO userSessionDAO = daoFactory.getUserSessionDAO();

    // Properties --------------------------------------------------------------------------------

    private UserSession userSession;
    private String username;
    private String password;

    // Actions -----------------------------------------------------------------------------------

    public void login() {
        try {

            // Do your "SELECT * FROM User WHERE username AND password" thing.
            User user = userDAO.find(username, password);

            if (user != null) {

                // User found. Put the User in the UserSession.
                userSession.setUser(user);

                // Do your "UPDATE UserSession SET values WHERE CookieID" thing.
                try {
                    userSessionDAO.save(userSession);

                    // Do your succes handling thing.
                    setSuccesMessage("You are logged in successfully!");
                } catch (DAOException e) {
                    // Do your exception handling thing.
                    setErrorMessage("Updating UserSession failed.", e);
                }
            } else {
                // Do your error handling thing.
                setErrorMessage("Unknown username and/or invalid password.");
            }
        } catch (DAOException e) {
            // Do your exception handling thing.
            setErrorMessage("Loading User failed.", e);
        }
    }

    public void logout() {

        // Just null out the user.
        userSession.setUser(null);

        try {
            // Do your "UPDATE UserSession SET values WHERE CookieID" thing.
            userSessionDAO.save(userSession);

            // Do your succes handling thing.
            setSuccesMessage("You are logged out successfully!");
        } catch (DAOException e) {
            // Do your exception handling thing.
            setErrorMessage("Updating UserSession failed.", e);
        }
    }

    // Getters and setters ------------------------------------------------------------------------

    // Implement default getters and setters here the usual way.

}

Declare the UserSession as a session scoped managed bean in the faces-config.xml and declare the UserForm as a request scoped bean with the UserSession instance as a managed property.

<managed-bean>
    <managed-bean-name>userSession</managed-bean-name>
    <managed-bean-class>mymodel.UserSession</managed-bean-class>
    <managed-bean-scope>session</managed-bean-scope>
</managed-bean>
<managed-bean>
    <managed-bean-name>userForm</managed-bean-name>
    <managed-bean-class>mycontroller.UserForm</managed-bean-class>
    <managed-bean-scope>request</managed-bean-scope>
    <managed-property>
        <property-name>userSession</property-name>
        <value>#{userSession}</value>
    </managed-property>
</managed-bean>

The UserSession#isLoggedIn() method can be very useful for JSF pages. You can use it in the rendered attribute of any component for example.

<h:panelGroup rendered="#{!userSession.loggedIn}">
    <h:outputText value="You are not logged in." />
    <%-- put the login code here --%>
</h:panelGroup>

<h:panelGroup rendered="#{userSession.loggedIn}">
    <h:outputText value="You are logged in as #{userSession.user.username}." />
    <%-- put the logout code here --%>
</h:panelGroup>
Back to top

Security considerations

Be aware that the cookie ID ought to be unique for every usersession. In the above example the cookie ID is a 128-bit autogenerated string obtained from java.util.UUID. Although this is extremely hard to wildguess/bruteforce, it may be a good practice to retrieve the IP address from the user using HttpServletRequest#getRemoteAddr() as well and put it along the cookie ID in the database (thus not as value in the cookie itself!). When looking up the usersession in the database you can use SELECT * FROM UserSession WHERE CookieID AND RemoteAddr. One big con is that this does not work very well when the user has a dynamic IP address. But it would be nice if such an option will be provided in the login page: "Lock this login to my IP address" or something.

Last note: the "official" container managed session ID as is in HttpSession#getId() is not alltime-unique. It is only unique inside the running webcontainer between all non-expired HttpSessions. So do not ever consider to use it as session ID for in the cookie!

Back to top

Copyright - There is no copyright on the code. You can copy, change and distribute it freely. Just mentioning this site should be fair.

(C) March 2007, BalusC

Monday, March 19, 2007

Facade POST by GET

Introduction

By default the JSF-generated forms only provides POST functionality. There is no default option to transform the POST forms (<form method="post">) into GET forms (<form method="get">). Not a big problem as far, as it is recommended to use POST only to handle form data. But it can sometimes be very useful to handle forms by GET, like for Search Results. So that you can bookmark or copypaste the URL in the address bar and reinvoke exact the same form action in another browser session.

You can't change POST by GET in JSF, but you can facade POST requests like it are GET requests using a PhaseListener which captures POST requests before the render response and redirects them to a GET request with the request parameters in the URL.

Back to top

Passing GET parameters to backing beans

This technique is also described in Communication. Here is how the JSF could look like:

<h:form>
    <h:inputText id="paramname1" value="#{myBean.paramname1}" />
    <h:inputText id="paramname2" value="#{myBean.paramname2}" />
    <h:commandButton value="submit" action="#{myBean.action}" />
    <h:outputText value="#{myBean.result}" />
</h:form>

Please note: it is important that the ID value of the UIInput component exactly the same is as the name of the bean property. The h:outputText component value is just an example to show the results. It can be anything, for example a h:dataTable is also possible.

Define the request/input parameters as managed properties in the faces-config.xml:

<managed-bean>
    <managed-bean-name>myBean</managed-bean-name>
    <managed-bean-class>mypackage.MyBean</managed-bean-class>
    <managed-bean-scope>request</managed-bean-scope>
    <managed-property>
        <property-name>paramname1</property-name>
        <value>#{param.paramname1}</value>
    </managed-property>
    <managed-property>
        <property-name>paramname2</property-name>
        <value>#{param.paramname2}</value>
    </managed-property>
</managed-bean>

As the request parameters are transferred by GET here, there is absolutely no need to put the managed bean in the session scope. If you really want to store some data in the session, then rather use the SessionMap from the ExternalContext or just define another managed bean which you put in the session scope and use this for session data only.

Here is how the backing bean MyBean.java look like. You can use the @PostConstruct annotation to process the GET parameters. The method with this annotation will only be invoked when the managed properties are all already set.

package mypackage;

import javax.annotation.PostConstruct;

public class MyBean {

    // Init --------------------------------------------------------------------------------------

    private String paramname1;
    private String paramname2;
    private String result;

    // Actions -----------------------------------------------------------------------------------

    @PostConstruct
    public void init() {
        // You can process the GET parameters here.
        result = paramname1 + ", " + paramname2;
    }

    public void action() {
        // You can do your form submit thing here.
    }

    // Getters -----------------------------------------------------------------------------------

    public String getParamname1() {
        return paramname1;
    }

    public String getParamname2() {
        return paramname2;
    }

    public String getResult() {
        return result;
    }

    // Setters -----------------------------------------------------------------------------------

    public void setParamname1(String paramname1) {
        this.paramname1 = paramname1;
    }

    public void setParamname2(String paramname2) {
        this.paramname2 = paramname2;
    }

}

The #{param} is a predefinied variable referring to the request parameter map. Invoking a GET request using the following URL will set the parameter values automatically in the managed bean instance and therefore also in the input fields, thanks to the managed-property configuration in the faces-config.xml:
http://example.com/mypage.jsf?paramname1=paramvalue1&paramname2=paramvalue2

Back to top

Facade POST requests like it are GET requests

This PhaseListener will facade POST requests like it are GET requests.

package mypackage;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.faces.FacesException;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.component.UIInput;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
import javax.servlet.http.HttpServletRequest;

/**
 * Facade POST requests like it are GET requests.
 * <p>
 * This phaselistener is designed to be used for JSF 1.2 with request scoped beans. The beans are 
 * expected to have the request parameters definied as managed properties in the faces-config.xml.
 * 
 * @author BalusC
 * @link http://balusc.blogspot.com/2007/03/facade-post-by-get.html
 */
public class PostFacadeGetListener implements PhaseListener {

    // Init --------------------------------------------------------------------------------------

    private static final String ALL_FACES_MESSAGES_ID = "PostFacadeGetListener.allFacesMessages";

    // Actions -----------------------------------------------------------------------------------

    /**
     * @see javax.faces.event.PhaseListener#getPhaseId()
     */
    public PhaseId getPhaseId() {

        // Only listen during the render response phase.
        return PhaseId.RENDER_RESPONSE;
    }

    /**
     * @see javax.faces.event.PhaseListener#beforePhase(javax.faces.event.PhaseEvent)
     */
    public void beforePhase(PhaseEvent event) {

        // Prepare.
        FacesContext facesContext = event.getFacesContext();
        HttpServletRequest request = (HttpServletRequest)
            facesContext.getExternalContext().getRequest();

        if ("POST".equals(request.getMethod())) {

            // Save facesmessages from POST request in session so that they'll be available on the
            // subsequent GET request.
            saveFacesMessages(facesContext);

            // Resolve action URL, add query parameters and redirect POST request to GET request.
            redirect(facesContext, addQueryParameters(facesContext, resolveActionURL(facesContext)));

        } else {

            // Restore any facesmessages in the GET request.
            restoreFacesMessages(facesContext);
        }
    }

    /**
     * @see javax.faces.event.PhaseListener#afterPhase(javax.faces.event.PhaseEvent)
     */
    public void afterPhase(PhaseEvent event) {
        // Nothing to do here.
    }

    // Helpers -----------------------------------------------------------------------------------

    /**
     * Save all facesmessages of the given facescontext in session.
     * @param facesContext The involved facescontext.
     */
    private static void saveFacesMessages(FacesContext facesContext) {

        // Prepare the facesmessages holder in the sessionmap. The LinkedHashMap has precedence over
        // HashMap, because in a LinkedHashMap the FacesMessages will be kept in order, which can be
        // very useful for certain error and focus handlings. Anyway, it's just your design choice.
        Map<String, List<FacesMessage>> allFacesMessages =
            new LinkedHashMap<String, List<FacesMessage>>();
        facesContext.getExternalContext().getSessionMap()
            .put(ALL_FACES_MESSAGES_ID, allFacesMessages);

        // Get client ID's of all components with facesmessages.
        Iterator<String> clientIdsWithMessages = facesContext.getClientIdsWithMessages();
        while (clientIdsWithMessages.hasNext()) {
            String clientIdWithMessage = clientIdsWithMessages.next();

            // Prepare client-specific facesmessages holder in the main facesmessages holder.
            List<FacesMessage> clientFacesMessages = new ArrayList<FacesMessage>();
            allFacesMessages.put(clientIdWithMessage, clientFacesMessages);

            // Get all messages from client and add them to the client-specific facesmessage list.
            Iterator<FacesMessage> facesMessages = facesContext.getMessages(clientIdWithMessage);
            while (facesMessages.hasNext()) {
                clientFacesMessages.add(facesMessages.next());
            }
        }
    }

    /**
     * Resolve the action URL of the current view of the given facescontext.
     * @param facesContext The involved facescontext.
     */
    private static String resolveActionURL(FacesContext facesContext) {

        // Obtain the action URL of the current view.
        return facesContext.getApplication().getViewHandler().getActionURL(
            facesContext, facesContext.getViewRoot().getViewId());
    }

    /**
     * Add POST parameters of the given facescontext as GET parameters to the given url.
     * @param facesContext The facescontext to obtain POST request parameters from.
     * @param url The URL to append the GET query parameters to.
     */
    private static String addQueryParameters(FacesContext facesContext, String url) {

        // Prepare.
        StringBuilder builder = new StringBuilder(url);
        int i = 0;

        // Gather the POST request parameters.
        Map<String, String> requestParameterMap = 
            facesContext.getExternalContext().getRequestParameterMap();

        // Walk through the POST request parameters and determine its source.
        for (String parameterKey : requestParameterMap.keySet()) {
            UIComponent component = facesContext.getViewRoot().findComponent(parameterKey);

            if (component instanceof UIInput) {
                // You may change this if-block if you want. This is done so, because the
                // requestParameterMap can contain more stuff than only UIInput values, for example
                // the UICommand element responsible for the action and the parent UIForm.

                // IMPORTANT: keep in mind that the values of HtmlInputSecret components will also
                // be passed to the GET here so that they would become visible in the address bar.
                // If you want to prevent this, then consider to set some specific request parameter
                // which should let this phaselistener skip the PRG completely for that request.

                // Append POST request parameters as GET query parameters to the URL.
                String parameterName = parameterKey.substring(parameterKey.lastIndexOf(':') + 1);
                String parameterValue = requestParameterMap.get(parameterKey);
                builder.append((i++ == 0 ? "?" : "&") + parameterName + "=" + parameterValue);
            }
        }
        
        return builder.toString();
    }

    /**
     * Invoke a redirect to the given URL.
     * @param facesContext The involved facescontext.
     */
    private static void redirect(FacesContext facesContext, String url) {
        try {
            // Invoke a redirect to the given URL.
            facesContext.getExternalContext().redirect(url);
        } catch (IOException e) {
            // Uhh, something went seriously wrong.
            throw new FacesException("Cannot redirect to " + url + " due to IO exception.", e);
        }
    }

    /**
     * Restore any facesmessages from session in the given FacesContext.
     * @param facesContext The involved FacesContext.
     */
    @SuppressWarnings("unchecked")
    private static void restoreFacesMessages(FacesContext facesContext) {

        // Remove all facesmessages from session.
        Map<String, List<FacesMessage>> allFacesMessages = (Map<String, List<FacesMessage>>)
            facesContext.getExternalContext().getSessionMap().remove(ALL_FACES_MESSAGES_ID);

        // If any, then restore them in the given facescontext.
        if (allFacesMessages != null) {
            for (String clientId : allFacesMessages.keySet()) {
                List<FacesMessage> allClientFacesMessages = allFacesMessages.get(clientId);
                for (FacesMessage clientFacesMessage : allClientFacesMessages) {
                    facesContext.addMessage(clientId, clientFacesMessage);
                }
            }
        }
    }

}

Activate this phaselistener by adding the following lines to the faces-config.xml:

<lifecycle>
    <phase-listener>mypackage.PostFacadeGetListener</phase-listener>
</lifecycle>

Now when you submit a form by POST using commandLink or commandButton, then it will automatically be redirected to a GET URL, with the POST parameters visible in the address bar, like if it was a GET request.

Back to top

Hide parameters

If you want to implement the PRG pattern, but you don't want to use visible UIInput components in the page, then just use h:inputHidden to hide the parameter and transfer it from request to request:

<h:form>
    <h:inputText id="paramname1" value="#{myBean.paramname1}" />
    <h:inputHidden id="paramname2" value="#{myBean.paramname2}" />
    <h:commandButton value="submit" action="#{myBean.action}" />
    <h:outputText value="#{myBean.result}" />
</h:form>

In this case, the value of the paramname2 is not visible on the page, but just hidden in a <input type="hidden"> element. And of course it is just visible in the GET request string of the URL.

Back to top

Copyright - There is no copyright on the code. You can copy, change and distribute it freely. Just mentioning this site should be fair.

(C) March 2007, BalusC

POST-Redirect-GET pattern

Notice

This article is targeted on JSF 1.2. For JSF 2.0, this can easier be achieved using the new Flash scope.

Doing the PRG in JSF

The POST-Redirect-GET pattern is commonly used in web applications to prevent double submit when refreshing a POST request and navigation problems/annoyances when using browser back/forward button to page between POST requests. Basically it works as follows: after processing the POST request (unnecessarily submitted/displayed form data and/or irritating "Are you sure to resend data?" popups), but right before sending any response to the client, redirect the response to a new GET request. This way refreshing the request won't (re)invoke the initial POST request anymore, but only the GET request.

JSF also supports it, you just need to add <redirect /> line to the navigation case so that it automatically invokes a redirect to the given view after POST. This is highly recommended when you're using commandlinks instead of outputlinks to navigate between pages (which I wouldn't call a good practice; POST should not be used for plain navigation, GET should be used for it). But in case of request based forms you will lost all submitted input values and the eventual FacesMessages. Not very handy if you want to redisplay submitted values after a succesful form submit and/or use FacesMessages to display error/succes message of a form submit.

Fortunately the problem of lost input values and FacesMessages is fixable with a PhaseListener. The below PhaseListener example will implement the PRG pattern that way so that for all POST requests all submitted input values and FacesMessages are saved in session for a once and are restored in the redirected view. All you need to do is just copypaste it and define it once in the faces-config of your JSF webapplication. No need to do any other configurations or make any changes in your JSF webapp.

package mypackage;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.faces.FacesException;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.component.UIInput;
import javax.faces.component.UIViewRoot;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
import javax.servlet.http.HttpServletRequest;

/**
 * Implement the POST-Redirect-GET pattern for JSF.
 * <p>
 * This phaselistener is designed to be used for JSF 1.2 with request scoped beans of which its
 * facesmessages and input values should be retained in the new GET request. If you're using session
 * scoped beans only, then you can safely remove the <tt>saveUIInputValues()</tt> and
 * <tt>restoreUIInputValues()</tt> methods to save (little) performance. If you're using JSF 1.1,
 * then you can also remove the <tt>saveViewRoot()</tt> and <tt>restoreViewRoot</tt> methods,
 * because it is not needed with its view state saving system.
 * 
 * @author BalusC
 * @link http://balusc.blogspot.com/2007/03/post-redirect-get-pattern.html
 */
public class PostRedirectGetListener implements PhaseListener {

    // Init ---------------------------------------------------------------------------------------

    private static final String PRG_DONE_ID = "PostRedirectGetListener.postRedirectGetDone";
    private static final String SAVED_VIEW_ROOT_ID = "PostRedirectGetListener.savedViewRoot";
    private static final String ALL_FACES_MESSAGES_ID = "PostRedirectGetListener.allFacesMessages";
    private static final String ALL_UIINPUT_VALUES_ID = "PostRedirectGetListener.allUIInputValues";

    // Actions ------------------------------------------------------------------------------------

    /**
     * @see javax.faces.event.PhaseListener#getPhaseId()
     */
    public PhaseId getPhaseId() {

        // Only listen during the render response phase.
        return PhaseId.RENDER_RESPONSE;
    }

    /**
     * @see javax.faces.event.PhaseListener#beforePhase(javax.faces.event.PhaseEvent)
     */
    public void beforePhase(PhaseEvent event) {

        // Prepare.
        FacesContext facesContext = event.getFacesContext();
        ExternalContext externalContext = facesContext.getExternalContext();
        HttpServletRequest request = (HttpServletRequest) externalContext.getRequest();
        Map<String, Object> sessionMap = externalContext.getSessionMap();

        if ("POST".equals(request.getMethod())) {

            // Save viewroot, facesmessages and UIInput values from POST request in session so that
            // they'll be available on the subsequent GET request.
            saveViewRoot(facesContext);
            saveFacesMessages(facesContext);
            saveUIInputValues(facesContext);

            // Redirect POST request to GET request.
            redirect(facesContext);
            
            // Set the PRG toggle.
            sessionMap.put(PRG_DONE_ID, true);

        } else if (sessionMap.containsKey(PRG_DONE_ID)) {

            // Restore any viewroot, facesmessages and UIInput values in the GET request.
            restoreViewRoot(facesContext);
            restoreFacesMessages(facesContext);
            restoreUIInputValues(facesContext);

            // Remove the PRG toggle.
            sessionMap.remove(PRG_DONE_ID);
        }
    }

    /**
     * @see javax.faces.event.PhaseListener#afterPhase(javax.faces.event.PhaseEvent)
     */
    public void afterPhase(PhaseEvent event) {
        // Do nothing.
    }

    // Helpers ------------------------------------------------------------------------------------

    /**
     * Save the current viewroot of the given facescontext in session. This is important in JSF 1.2,
     * because the viewroot would be lost in the new GET request and will only be created during
     * the afterPhase of RENDER_RESPONSE. But as we need to restore the input values in the 
     * beforePhase of RENDER_RESPONSE, we have to save and restore the viewroot first ourselves.
     * @param facesContext The involved facescontext.
     */
    private static void saveViewRoot(FacesContext facesContext) {
        UIViewRoot savedViewRoot = facesContext.getViewRoot();
        facesContext.getExternalContext().getSessionMap()
            .put(SAVED_VIEW_ROOT_ID, savedViewRoot);
    }

    /**
     * Save all facesmessages of the given facescontext in session. This is done so because the
     * facesmessages are purely request scoped and would be lost in the new GET request otherwise.
     * @param facesContext The involved facescontext.
     */
    private static void saveFacesMessages(FacesContext facesContext) {

        // Prepare the facesmessages holder in the sessionmap. The LinkedHashMap has precedence over
        // HashMap, because in a LinkedHashMap the FacesMessages will be kept in order, which can be
        // very useful for certain error and focus handlings. Anyway, it's just your design choice.
        Map<String, List<FacesMessage>> allFacesMessages =
            new LinkedHashMap<String, List<FacesMessage>>();
        facesContext.getExternalContext().getSessionMap()
            .put(ALL_FACES_MESSAGES_ID, allFacesMessages);

        // Get client ID's of all components with facesmessages.
        Iterator<String> clientIdsWithMessages = facesContext.getClientIdsWithMessages();
        while (clientIdsWithMessages.hasNext()) {
            String clientIdWithMessage = clientIdsWithMessages.next();

            // Prepare client-specific facesmessages holder in the main facesmessages holder.
            List<FacesMessage> clientFacesMessages = new ArrayList<FacesMessage>();
            allFacesMessages.put(clientIdWithMessage, clientFacesMessages);

            // Get all messages from client and add them to the client-specific facesmessage list.
            Iterator<FacesMessage> facesMessages = facesContext.getMessages(clientIdWithMessage);
            while (facesMessages.hasNext()) {
                clientFacesMessages.add(facesMessages.next());
            }
        }
    }

    /**
     * Save all input values of the given facescontext in session. This is done specific for request
     * scoped beans, because its properties would be lost in the new GET request otherwise.
     * @param facesContext The involved facescontext.
     */
    private static void saveUIInputValues(FacesContext facesContext) {

        // Prepare the input values holder in sessionmap.
        Map<String, Object> allUIInputValues = new HashMap<String, Object>();
        facesContext.getExternalContext().getSessionMap()
            .put(ALL_UIINPUT_VALUES_ID, allUIInputValues);

        // Pass viewroot children to the recursive method which saves all input values.
        saveUIInputValues(facesContext, facesContext.getViewRoot().getChildren(), allUIInputValues);
    }

    /**
     * A recursive method to save all input values of the given facescontext in session.
     * @param facesContext The involved facescontext.
     */
    private static void saveUIInputValues(
        FacesContext facesContext, List<UIComponent> components, Map<String, Object> allUIInputValues)
    {
        // Walk through the components and if it is an instance of UIInput, then save the value.
        for (UIComponent component : components) {
            if (component instanceof UIInput) {
                UIInput input = (UIInput) component;
                allUIInputValues.put(input.getClientId(facesContext), input.getValue());
            }

            // Pass the children of the current component back to this recursive method.
            saveUIInputValues(facesContext, component.getChildren(), allUIInputValues);
        }
    }

    /**
     * Invoke a redirect to the same URL as the current action URL.
     * @param facesContext The involved facescontext.
     */
    private static void redirect(FacesContext facesContext) {

        // Obtain the action URL of the current view.
        String url = facesContext.getApplication().getViewHandler().getActionURL(
            facesContext, facesContext.getViewRoot().getViewId());

        try {
            // Invoke a redirect to the action URL.
            facesContext.getExternalContext().redirect(url);
        } catch (IOException e) {
            // Uhh, something went seriously wrong.
            throw new FacesException("Cannot redirect to " + url + " due to IO exception.", e);
        }
    }

    /**
     * Restore any viewroot from session in the given facescontext.
     * @param facesContext The involved FacesContext.
     */
    private static void restoreViewRoot(FacesContext facesContext) {

        // Remove the saved viewroot from session.
        UIViewRoot savedViewRoot = (UIViewRoot)
            facesContext.getExternalContext().getSessionMap().remove(SAVED_VIEW_ROOT_ID);

        // Restore it in the given facescontext.
        facesContext.setViewRoot(savedViewRoot);
    }

    /**
     * Restore any facesmessages from session in the given FacesContext.
     * @param facesContext The involved FacesContext.
     */
    @SuppressWarnings("unchecked")
    private static void restoreFacesMessages(FacesContext facesContext) {

        // Remove all facesmessages from session.
        Map<String, List<FacesMessage>> allFacesMessages = (Map<String, List<FacesMessage>>)
            facesContext.getExternalContext().getSessionMap().remove(ALL_FACES_MESSAGES_ID);

        // Restore them in the given facescontext.
        for (Entry<String, List<FacesMessage>> entry : allFacesMessages.entrySet()) {
            for (FacesMessage clientFacesMessage : entry.getValue()) {
                facesContext.addMessage(entry.getKey(), clientFacesMessage);
            }
        }
    }

    /**
     * Restore any input values from session in the given FacesContext.
     * @param facesContext The involved FacesContext.
     */
    @SuppressWarnings("unchecked")
    private static void restoreUIInputValues(FacesContext facesContext) {

        // Remove all input values from session.
        Map<String, Object> allUIInputValues = (Map<String, Object>)
            facesContext.getExternalContext().getSessionMap().remove(ALL_UIINPUT_VALUES_ID);

        // Restore them in the given facescontext.
        for (Entry<String, Object> entry : allUIInputValues.entrySet()) {
            UIInput input = (UIInput) facesContext.getViewRoot().findComponent(entry.getKey());
            input.setValue(entry.getValue());
        }
    }

}

Activate this phaselistener by adding the following lines to the faces-config.xml:

<lifecycle>
    <phase-listener>mypackage.PostRedirectGetListener</phase-listener>
</lifecycle>

Now when you submit a form by POST using commandLink or commandButton, then it will automatically be redirected to a GET request, hereby keeping the submitted input values and FacesMessages in the new GET request.

Back to top

Copyright - There is no copyright on the code. You can copy, change and distribute it freely. Just mentioning this site should be fair.

(C) March 2007, BalusC

Friday, March 2, 2007

JDBC tutorial (Dutch)

Introductie

Java kent een aantal standaard klassen waarmee je met SQL databases kunt communiceren, deze klassen (eigenlijk interfaces) zitten in het java.sql package onder de noemer "JDBC API" (Java DataBase Connectivity Application Programming Interface). Hieronder staan enkele klassen en methoden die van belang zijn bij de communicatie met SQL databases.

java.sql klasse beschrijving
DriverManager Beheert de JDBC drivers.
Connection Opent een sessie met een specifieke database.
Statement Een open statement om SQL queries mee te versturen en resultaten te ontvangen. De volgende twee methoden zijn van nut:
executeQuery(sql): nuttig voor SELECT queries en retourneert een ResultSet.
executeUpdate(sql): nuttig voor INSERT, UPDATE en DELETE queries en retourneert de hoeveelheid getroffen rijen in vorm van een int.
PreparedStatement Een open statement met een voorbereide SQL query waaraan je slechts de waarden hoeft toe te voegen. Zeer nuttig om eventuele SQL injecties te voorkomen doordat je met specifieke objecten kunt werken in plaats van met strings. Bij de stringconversies worden de aanhalingstekens automatisch opgevangen. De volgende twee methoden zijn van nut:
executeQuery(): nuttig voor SELECT queries en retourneert een ResultSet.
executeUpdate(): nuttig voor INSERT, UPDATE en DELETE queries en retourneert de hoeveelheid getroffen rijen in vorm van een int.
ResultSet Het resultaat van Statement#executeQuery() zal in een ResultSet gezet worden.

Het enige extra wat je buiten de standaard JDBC API nodig hebt is een geldige JDBC driver voor de gewenste database. De JDBC driver is grof gezegd de concrete implementatie van de JDBC API, dat bijna louter uit interfaces bestaat. Deze kun je normaliter als een JAR bestand downloaden, die je dan in de classpath moet zetten. Voorbeelden: MySQL Connector/J, DB2 Driver for JDBC and SQLJ, Oracle JDBC Driver, PostgreSQL JDBC Driver, Microsoft SQL Server JDBC Driver, etcetera.

Terug naar boven

Foutenafhandeling

Praktisch alle klassen van de java.sql package kunnen een SQLException of een subklasse daarvan afwerpen. Je bent dus verplicht om de SQL acties in een try-catch-finally statement te zetten. Anders moet je desbetreffende SQLException doorgooien via de throws bepaling van de methode.

Het is zeer aanbevelenswaardig om de database sessie (Connection) na het gebruik altijd te sluiten met de close() methode om de systeembronnen te vrijgeven. Deze wordt namelijk niet automatisch direct na gebruik gesloten. Als je dat niet doet, dan kun je wanneer je veel connecties maakt na een tijdje een tekort aan systeembronnen krijgen, met alle desastreuze gevolgen van dien.

Wanneer je de Connection sluit, dan zullen de 'goede' JDBC drivers ook alle binnen dezelfde sessie geopende statementen en resultsets ook automatisch gesloten worden. Wanneer je een afzonderlijke Statement of PreparedStatement binnen dezelfde sessie sluit, dan zullen de 'goede' JDBC drivers alle geopende resultsets ook automatisch sluiten, maar de connectie blijft nog wel open. Wanneer je een afzonderlijke ResultSet binnen dezelfde sessie sluit, dan zul je er niet meer doorheen kunnen lopen, maar blijven de connectie en de statementen nog wel open. Wanneer je binnen een connectie meerdere statementen en/of resultsets wilt gaan openen, dan is het verstandig om deze direct te sluiten na het het verwerken ervan, want de hoeveelheid tegelijkertijd geopende statementen en resultsets is niet ongelimiteerd. Wanneer je binnen een statement meerdere resultsets wilt gaan openen, dan zullen de 'goede' JDBC drivers de eerder geopende resultsets automatisch sluiten.

Ondanks dat 'goede' JDBC drivers voor het automatisch sluiten zorgen, is het jouw taak als een 'goede' developer om alle connecties, statementen en resultsets zelf te sluiten! Anders breekt jouw hele applicatie wanneer iemand een 'slechte' JDBC driver gebruikt.

Terug naar boven

JDBC driver installeren

Hier zullen we van een MySQL database uitgaan. MySQL is de meest gebruikte freeware database. Hoe MySQL te installeren valt buiten de scope van dit artikel. Het wijst praktisch van zichzelf uit: even MySQL downloaden en dan de installer uitvoeren.

Om een SQL database vanuit Java te kunnen benaderen zul je dus de bijbehorende JDBC driver moeten downloaden en installeren, voor MySQL is dit de MySQL Connector/J. Download bij voorkeur de meest recente versie, op het moment van schrijven is dit versie 5.0. Download hier het zip bestand. Pak deze zip uit en haal daar het JAR bestand uit, in dit geval heet deze mysql-connector-java-5.0.8-bin.jar (let op: de versie nummer kan verschillen).

Update: er is een nieuwere Connector/J beschikbaar, de versie 5.1. Hiervoor heb je echter minimaal Java 6.0 nodig, aangezien de JDBC 4.0 specificatie pas in Java 6.0 is geintroduceerd. De Connector/J 5.0 werkt nog wel op Java 5.0 met de JDBC 3.0 specificatie.

Om deze driver in je Java code te kunnen gebruiken, zul je deze eerst in de classpath moeten zetten. Wanneer je het lokaal buiten de IDE wil gebruiken, dan zul je mogelijk eerst de omgevingsvariabele classpath moeten definieren waar je de JAR's kunt neerplanten, dit staat hier uitgelegd: Uitvoeren - Classpath. Wanneer je het in een applicatieserver wil gebruiken, dan zul je deze JAR's in de classpath van de applicatieserver moeten zetten, gewoonlijk is dit de /lib directory. Binnen een IDE, zoals Eclipse, volstaat het om deze driver aan de Java Build Path toe te voegen, zie de onderstaande procedure:

  1. Maak eventueel een project aan: File - New - Project... - Java Project, klik op Next, geef het een naam, bijvooorbeeld "MyDao", laat de rest van de velden standaard en klik op Finish.
  2. Importeer de JAR; rechtsklik op dit project: Import... - General - File System, klik op Next, wijs de directory aan waar het mysql-connector-java-5.0.8-bin.jar bestand zit, vink het bestand aan en klik op Finish.
    Import MySQL JAR
  3. Voeg de JAR tenslotte toe aan de classpath van het project; rechtsklik op het project - Properties - Java Build Path - Libraries - Add JARs - selecteer de zojuist geimporteerde JAR en klik op Finish.
    MySQL JAR in Build PathMySQL JAR in Project

Voor alle andere databases geldt hetzelfde procedure: download de JAR(s) en zet het in de classpath. Bij sommige database servers worden inderdaad meerdere JAR's geleverd, zoals bij DB2.

Terug naar boven

Database voorbereiden

Ter voorbereiding: we gaan hier ervan uit dat je de SQL basics onder de knie hebt. JDBC en SQL staan op zich volledig los van elkaar: de SQL queries moet je zelf schrijven, dat doet JDBC niet voor jou. Het verzorgt slechts de communicatie tussen de Java code en de SQL database. Mocht SQL jou ook niet helemaal bekend zijn, dan kan het waard zijn jezelf daar eerst in te verdiepen: SQL tutorials op het Internet.

We zullen nu eerst even een voorbeeld MySQL database tabel met een auto-generated technische ID veld "ID", een alfanummeriek veld "Name" en een nummeriek veld "Value" voorbereiden. Voer de onderstaande MySQL SQL uit in de database:

CREATE DATABASE javabase;
CREATE TABLE javabase.maintable (
    ID BIGINT AUTO_INCREMENT PRIMARY KEY,
    Name VARCHAR(255),
    Value INT
);

Let op: de SQL CREATE commando's kunnen per database verschillen. Voor bijvoorbeeld DB2, Oracle, PostgreSQL en Microsoft SQL Server moet je zo'n tabel op een iets andere manier aanmaken, omdat ze met databaseschema's werken, iets wat MySQL nog volledig onbekend is. Daarnaast heeft iedere database een andere implementatie van een auto-generated ID veld. Hier staat een rijke bron aan database-specifieke SQL commando's: SQLzoo.net.

Hieronder staat hoe je een vergelijkbaar tabel in DB2 kunt aanmaken:

CREATE DATABASE javabase;
CREATE SCHEMA schema;
CREATE TABLE javabase.schema.maintable (
    ID BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    Name VARCHAR(255),
    Value INT
);

En de Oracle en PostgreSQL equivalent:

CREATE DATABASE javabase;
CREATE SCHEMA schema;
CREATE TABLE javabase.schema.maintable (
    ID SERIAL PRIMARY KEY,
    Name VARCHAR(255),
    Value INT
);

Noot: deze query maakt ook automatisch een sequence aan in javabase.schema.maintable_id_seq.

En tenslotte die voor de Microsoft SQL Server:

CREATE DATABASE javabase;
CREATE SCHEMA schema;
CREATE TABLE javabase.schema.maintable (
    ID BIGINT IDENTITY PRIMARY KEY,
    Name VARCHAR(255),
    Value INT
);
Terug naar boven

JDBC URL voorbereiden

Om een database tabel vanuit de Java code te kunnen aanroepen heb je een JDBC URL nodig. Deze is in het geval van MySQL databases als volgt opgebouwd:

jdbc:mysql://hostname:port/database?user=username&password=password

hostname: verplicht, de host naam of de IP. Bijvoorbeeld: localhost of 127.0.0.1
port: optioneel, de poort van de database. Bijvoorbeeld: 3306
database: verplicht, de naam van de database. Bijvoorbeeld: javabase
username: optioneel, de inlog naam voor de database. Bijvoorbeeld: root
password: optioneel, de wachtwoord voor de database. Bijvoorbeeld: d$7hF_r!9Y

Uitgaande van een MySQL database op je eigen computer (localhost) achter de standaard poort 3306 (die je eigenlijk gewoon kunt weglaten) en een gebruikeraccount root met de wachtwoord d$7hF_r!9Y, zou onze MYSQL JDBC URL er zo uitzien:

  jdbc:mysql://localhost:3306/javabase?user=root&password=d$7hF_r!9Y

Let op: dit verschilt dus per database type, zie ook de documentatie bij de JDBC driver. De onderstaande voorbeelden zijn uit desbetreffende documentatie gehaald. Wanneer deze URL's om een of andere reden niet lekker werken, dan zul je het beste even zelf de JDBC documentatie moeten doornemen.

Voor een DB2 server die standaard op poort 50000 zit ziet een vergelijkbare JDBC URL als volgt eruit:

  jdbc:db2://localhost:50000/javabase:user=root;password=d$7hF_r!9Y

Voor Oracle die standaard achter poort 1521 bereikbaar is moet je de JDBC URL als volgt formuleren:

  jdbc:oracle:thin:root/d$7hF_r!9Y@//localhost:1521/javabase

Voor PostgreSQL gelden dezelfde richtlijnen als MySQL, behalve dan dat het als standaard poort 5432 gebruikt:

  jdbc:postgresql://localhost:5432/javabase?user=root&password=d$7hF_r!9Y

Voor de Microsoft SQL Server die standaard op poort 1433 zit zou zo'n JDBC URL als volgt eruitzien:

  jdbc:sqlserver://localhost:1344;databaseName=javabase;user=root;password=d$7hF_r!9Y

Terug naar boven

Eenvoudige INSERT

Hieronder volgt een compleet werkend voorbeeld van een eenvoudige INSERT actie op de database.

package test;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class Test {

    public static void main(String[] args) {

        // Laad eerst de driver.
        try {
            Class.forName("com.mysql.jdbc.Driver");

            // Voor DB2 laad je de driver als volgt:
            // Class.forName("com.ibm.db2.jcc.DB2Driver");

            // Voor Oracle laad je de driver als volgt:
            // Class.forName("oracle.jdbc.driver.OracleDriver");

            // Voor PostgreSQL laad je de driver als volgt:
            // Class.forName("org.postgresql.Driver");

            // Voor Microsoft SQL Server laad je de driver als volgt:
            // Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver");

            System.out.println("Laden van de driver is gelukt.");
        } catch (ClassNotFoundException e) {
            System.err.println("Kan de driver niet in de classpath vinden.");
            e.printStackTrace();
            return;
        }

        // Voorbereid de connectie, statement, resultset, URL en INSERT query.
        Connection connection = null;
        Statement statement = null;
        ResultSet generatedKeys = null;
        String url = "jdbc:mysql://localhost/javabase?user=root&password=d$7hF_r!9Y";
        String insertQuery = "INSERT INTO maintable (name, value) VALUES ('testnaam', 10)";

        // Voor DB2, Oracle, PostgreSQL en MSSQL moet je ook de database schema aanwijzen:
        // INSERT INTO schema.maintable (name, value) VALUES ('testnaam', 10)

        try {
            // Verkrijg de connectie.
            connection = DriverManager.getConnection(url);

            // Verkrijg de statement.
            statement = connection.createStatement();

            // Voer de INSERT query uit.
            int affectedRows = statement.executeUpdate(insertQuery);

            // Verkrijg de INSERT ID.
            if (affectedRows == 1) {
                generatedKeys = statement.getGeneratedKeys();

                // Let op: het is afhankelijk van de JDBC driver of dit werkt! Bijvoorbeeld de
                // Oracle en PostgreSQL JDBC drivers ondersteunen dit niet. Je zult deze als
                // sequence moeten opvragen, waarbij de sequence naam in regel als volgt is:
                // "schemanaam.tabelnaam" + "_id_seq". Doe dit wel binnen dezelfde statement!
                // 
                // String sequenceQuery = "SELECT currval('schema.maintable_id_seq')";
                // generatedKeys = statement.executeQuery(sequenceQuery);

                if (generatedKeys.next()) {
                    long insertID = generatedKeys.getLong(1);
                    System.out.println("Insert ID is: " + insertID);
                }
            }

            // Klaar!
            System.out.println("Uitvoeren van de INSERT query is gelukt.");
        } catch (SQLException e) {
            // Foutje?
            System.err.println("Uitvoeren van de INSERT query is mislukt.");
            e.printStackTrace();
        } finally {
            // Sluit de resultset, statement en connectie. Doe dit altijd in de finally blok!
            if (generatedKeys != null) {
                try {
                    generatedKeys.close();
                } catch (SQLException e) {
                    // Niks aan te doen.
                    System.err.println("Kan de resultset niet sluiten.");
                    e.printStackTrace();
                }
            }
            if (statement != null) {
                try {
                    statement.close();
                } catch (SQLException e) {
                    // Niks aan te doen.
                    System.err.println("Kan de statement niet sluiten.");
                    e.printStackTrace();
                }
            }
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    // Niks aan te doen.
                    System.err.println("Kan de connectie niet sluiten.");
                    e.printStackTrace();
                }
            }
        }
    }

}

Laden van de driver is gelukt.
Insert ID is: 1
Uitvoeren van de INSERT query is gelukt.

Je kunt de connectie ook op een andere manier verkrijgen, de DriverManager.getConnection() is ook op de volgende manier te gebruiken:

        ...

        // Voorbereid de URL, de gebruikersnaam en de wachtwoord.
        String url = "jdbc:mysql://localhost/javabase";
        String username = "root";
        String password = "d$7hF_r!9Y";

        try {
            // Verkrijg de connectie.
            connection = DriverManager.getConnection(url, username, password);

            ...

Je kunt dus de username en de password gedeelten uit de JDBC URL weghalen en deze apart doorgeven.

Omdat het sluiten van de connectie, statement en resultset vaker dan eens zal gebeuren, is het handiger om deze te refactoren naar een utility klasse met static methoden dat meer dan eens aangeroepen kan worden. Hieronder staat een voorbeeld:

package test;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public final class SqlUtil {

    public static void close(Connection connection) {
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                // Niks aan te doen.
                System.err.println("Kan de connectie niet sluiten.");
                e.printStackTrace();
            }
        }
    }

    public static void close(Statement statement) {
        if (statement != null) {
            try {
                statement.close();
            } catch (SQLException e) {
                // Niks aan te doen.
                System.err.println("Kan de statement niet sluiten.");
                e.printStackTrace();
            }
        }
    }

    public static void close(ResultSet resultSet) {
        if (resultSet != null) {
            try {
                resultSet.close();
            } catch (SQLException e) {
                // Niks aan te doen.
                System.err.println("Kan de resultset niet sluiten.");
                e.printStackTrace();
            }
        }
    }

}

Die kun je dan als volgt in de finally blok gebruiken:

            ...

        } finally {
            // Sluit de resultset, statement en connectie. Doe dit altijd in de finally blok!
            SqlUtil.close(generatedKeys);
            SqlUtil.close(statement);
            SqlUtil.close(connection);
        }

Terug naar boven

Eenvoudige SELECT

Hieronder volgt een compleet werkend voorbeeld van een eenvoudige SELECT actie op de database.

package test;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class Test {

    public static void main(String[] args) {

        // Laad eerst de driver.
        try {
            Class.forName("com.mysql.jdbc.Driver");
            System.out.println("Laden van de driver is gelukt.");
        } catch (ClassNotFoundException e) {
            System.err.println("Kan de driver niet in de classpath vinden.");
            e.printStackTrace();
            return;
        }

        // Voorbereid de connectie, statement, resultset, URL en SELECT query.
        Connection connection = null;
        Statement statement = null;
        ResultSet resultSet = null;
        String url = "jdbc:mysql://localhost/javabase?user=root&password=d$7hF_r!9Y";
        String selectQuery = "SELECT id, name, value FROM maintable WHERE name = 'testnaam'";

        try {
            // Verkrijg de connectie.
            connection = DriverManager.getConnection(url);

            // Verkrijg de statement.
            statement = connection.createStatement();

            // Voer de SELECT query uit.
            resultSet = statement.executeQuery(selectQuery);

            // Verwerk de resultaten.
            while (resultSet.next()) {
                long id = resultSet.getLong("id");
                String name = resultSet.getString("name");
                int value = resultSet.getInt("value");
                System.out.println("ID=" + id + ",Name=" + name + ",Value=" + value);
            }

            // Klaar!
            System.out.println("Uitvoeren van de SELECT query is gelukt.");
        } catch (SQLException e) {
            // Foutje?
            System.err.println("Uitvoeren van de SELECT query is mislukt.");
            e.printStackTrace();
        } finally {
            // Sluit de resultset, statement en connectie. Doe dit altijd in de finally blok!
            SqlUtil.close(resultSet);
            SqlUtil.close(statement);
            SqlUtil.close(connection);
        }
    }

}

Laden van de driver is gelukt.
ID=1,Name=testnaam,Value=10
ID=2,Name=testnaam,Value=10
ID=3,Name=testnaam,Value=10
Uitvoeren van de SELECT query is gelukt.

De eerder beschreven INSERT query werd inderdaad eerst driemaal uitgevoerd ;) De ResultSet van de SELECT query kun je trouwens ook op de volgende manier verwerken, met kolomnummers in plaats van kolomnamen. Hierbij kun je gewoon de volgorde van de kolommen van de database aanhouden en het begint altijd met 1 (en dus niet met 0!).

            // Verwerk de resultaten.
            while (resultSet.next()) {
                long id = resultSet.getLong(1);
                String name = resultSet.getString(2);
                int value = resultSet.getInt(3);
                System.out.println("ID=" + id + ",Name=" + name + ",Value=" + value);
            }

Deze methode is fractioneel sneller dan het gebruik van volledige kolomnamen, het scheelt in geval van de betere JDBC drivers ongeveer een halve procent tot één procent van de tijd.

Als je er niet helemaal zeker van bent in welk soort datatype of object je de verkregen waarde moet stoppen en/of wanneer je ClassCastException foutmeldingen krijgt, dan kun je ook proefondervindelijk getObject() van de resultset gebruiken om het resultaat te upcasten naar een Object. Met Object#getClass() kun je tenslotte de geinstantieerde klasse opvragen en derhalve de 'automatisch' door de driver vertaalde klasse achterhalen:

            // Achterhaal het vertaalde object type van de ID veld.
            if (resultSet.next()) {
                Object id = resultSet.getObject("ID");
                System.out.println(id.getClass());
            }

class java.lang.Long

In dit geval wordt een MySQL BIGINT veld dus door de JDBC driver geconverteerd naar een Long. Je zou de waarde dus het beste in het primitieve datatype long of in de wrapper datatype object Long moeten stoppen.

Terug naar boven

Eenvoudige UPDATE

Hieronder volgt een compleet werkend voorbeeld van een eenvoudige UPDATE actie op de database.

package test;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class Test {

    public static void main(String[] args) {

        // Laad eerst de driver.
        try {
            Class.forName("com.mysql.jdbc.Driver");
            System.out.println("Laden van de driver is gelukt.");
        } catch (ClassNotFoundException e) {
            System.err.println("Kan de driver niet in de classpath vinden.");
            e.printStackTrace();
            return;
        }

        // Voorbereid de connectie, statement, URL en UPDATE query.
        Connection connection = null;
        Statement statement = null;
        String url = "jdbc:mysql://localhost/javabase?user=root&password=d$7hF_r!9Y";
        String updateQuery = "UPDATE maintable SET name = 'anderenaam' WHERE id = 1";

        try {
            // Verkrijg de connectie.
            connection = DriverManager.getConnection(url);

            // Verkrijg de statement.
            statement = connection.createStatement();

            // Voer de UPDATE query uit.
            int affectedRows = statement.executeUpdate(updateQuery);
            System.out.println("Aantal getroffen rijen: " + affectedRows);

            // Klaar!
            System.out.println("Uitvoeren van de UPDATE query is gelukt.");
        } catch (SQLException e) {
            // Foutje?
            System.err.println("Uitvoeren van de UPDATE query is mislukt.");
            e.printStackTrace();
        } finally {
            // Sluit de statement en connectie. Doe dit altijd in de finally blok!
            SqlUtil.close(statement);
            SqlUtil.close(connection);
        }
    }

}

Laden van de driver is gelukt.
Aantal getroffen rijen: 1
Uitvoeren van de UPDATE query is gelukt.

Terug naar boven

Eenvoudige DELETE

Hieronder volgt een compleet werkend voorbeeld van een eenvoudige DELETE actie op de database.

package test;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class Test {

    public static void main(String[] args) {

        // Laad eerst de driver.
        try {
            Class.forName("com.mysql.jdbc.Driver");
            System.out.println("Laden van de driver is gelukt.");
        } catch (ClassNotFoundException e) {
            System.err.println("Kan de driver niet in de classpath vinden.");
            e.printStackTrace();
            return;
        }

        // Voorbereid de connectie, statement, URL en DELETE query.
        Connection connection = null;
        Statement statement = null;
        String url = "jdbc:mysql://localhost/javabase?user=root&password=d$7hF_r!9Y";
        String deleteQuery = "DELETE FROM maintable WHERE name = 'anderenaam'";

        try {
            // Verkrijg de connectie.
            connection = DriverManager.getConnection(url);

            // Verkrijg de statement.
            statement = connection.createStatement();

            // Voer de DELETE query uit.
            int affectedRows = statement.executeUpdate(deleteQuery);
            System.out.println("Aantal getroffen rijen: " + affectedRows);

            // Klaar!
            System.out.println("Uitvoeren van de DELETE query is gelukt.");
        } catch (SQLException e) {
            // Foutje?
            System.err.println("Uitvoeren van de DELETE query is mislukt.");
            e.printStackTrace();
        } finally {
            // Sluit de statement en connectie. Doe dit altijd in de finally blok!
            SqlUtil.close(statement);
            SqlUtil.close(connection);
        }
    }

}

Laden van de driver is gelukt.
Aantal getroffen rijen: 1
Uitvoeren van de DELETE query is gelukt.

Terug naar boven

Voorbereide statementen

Met PreparedStatement kun je een statement voorbereiden, waarbij je de openstaande waarden vult met een vraagteken "?". Dit is niet alleen handig voor veelgebruikte statementen, maar ook om SQL injecties te voorkomen, bij de stringconversies binnen de PreparedStatement worden de aanhalingstekens namelijk automatisch opgevangen. Hieronder volgt een voorbeeld van een voorbereide INSERT statement. De waarden moet je in de volgorde toevoegen zoals de vraagtekens in de voorbereide statement staan en de index begint altijd met 1 (en dus niet met 0!).

package test;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class Test {

    public static void main(String[] args) {

        try {
            Class.forName("com.mysql.jdbc.Driver");
            System.out.println("Laden van de driver is gelukt.");
        } catch (ClassNotFoundException e) {
            System.err.println("Kan de driver niet in de classpath vinden.");
            e.printStackTrace();
            return;
        }

        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet generatedKeys = null;
        String url = "jdbc:mysql://localhost/javabase?user=root&password=d$7hF_r!9Y";

        // Voorbereid de INSERT query.
        String preparedInsertQuery = "INSERT INTO maintable (name, value) VALUES (?, ?)";

        try {
            connection = DriverManager.getConnection(url);

            // Verkrijg de voorbereide statement.
            preparedStatement = connection.prepareStatement(preparedInsertQuery);

            // Voeg de waarden toe.
            preparedStatement.setString(1, "testname");
            preparedStatement.setInt(2, 10);

            // Voer de INSERT query uit.
            preparedStatement.executeUpdate();

            generatedKeys = preparedStatement.getGeneratedKeys();
            if (generatedKeys.next()) {
                long insertID = generatedKeys.getLong(1);
                System.out.println("Insert ID is: " + insertID);
            }

            System.out.println("Uitvoeren van de INSERT query is gelukt.");
        } catch (SQLException e) {
            System.err.println("Uitvoeren van de INSERT query is mislukt.");
            e.printStackTrace();
        } finally {
            SqlUtil.close(generatedKeys);
            SqlUtil.close(preparedStatement);
            SqlUtil.close(connection);
        }
    }

}

Laden van de driver is gelukt.
Insert ID is: 4
Uitvoeren van de INSERT query is gelukt.

Noot: de SqlUtil hoeft niet uitgebreid te worden met een nieuwe close() methode voor de PreparedStatement. Aangezien deze een subklasse is van Statement, wordt de close() methode daarvan gewoon gebruikt.

Terug naar boven

DTO's zijn een must

Het is een zeer goede practice om het verkregen ResultSet te omvertalen naar een lijst met DTO's (Data Transfer Objects). Een DTO moet in dit geval een volledige rij van een database tabel voorstellen. De DTO's zouden de Javabean specificatie moeten volgen: de velden (properties) worden private gemaakt en deze zijn alleen toegankelijk via public getters en setters (de accessors). Dit is overigens het schoolvoorbeeld van encapsulation. Hieronder volgt een voorbeeld van een DTO afspiegeling van de "maintable" tabel:

package test;

public class Maintable {

    // Properties -------------------------------------------------------------------------------

    private Long id;
    private String name;
    private Integer value;

    // Getters ----------------------------------------------------------------------------------

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public Integer getValue() {
        return value;
    }

    // Setters ----------------------------------------------------------------------------------

    public void setId(Long id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setValue(Integer value) {
        this.value = value;
    }

    // Helpers ----------------------------------------------------------------------------------

    // Dit is niet verplicht, maar gewoon handig. Het overschrijft de
    // Object#toString() zodat je een mooie String representatie krijgt.
    public String toString() {
        return "[ID=" + id + ",Name=" + name + ",Value=" + value + "]";
    }

}

Het kan handig zijn om wrapper datatype objecten (Long, Integer, Boolean, etc) in plaats van primitieve datatypen (long, int, boolean, etc) voor de properties te gebruiken, met name omdat deze wrapper datatype objecten in tegenstelling tot primitieve datatypen ook null waarden kunnen bevatten, waarmee je zou kunnen aangeven dat het veld nooit is ingevuld. Daarnaast kunnen de "primitieve velden" van de database ook NULL waarden bevatten danwel accepteren die je op geen enkele manier naar een primitieve datatype kunt vertalen. Pas wanneer een database veld strikt als NOT NULL is gespecificeerd, dan kun je daarvoor wel gerust een primitieve datatype gebruiken. Zie verder ook Java Tutorial - Datatypen en Data conversies.

Deze DTO kun je in het voorbeeld van een voorbereide SELECT als volgt toepassen:

package test;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

public class Test {

    public static void main(String[] args) {

        try {
            Class.forName("com.mysql.jdbc.Driver");
            System.out.println("Laden van de driver is gelukt.");
        } catch (ClassNotFoundException e) {
            System.err.println("Kan de driver niet in de classpath vinden.");
            e.printStackTrace();
            return;
        }

        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        String url = "jdbc:mysql://localhost/javabase?user=root&password=d$7hF_r!9Y";
        String preparedSelectQuery = "SELECT id, name, value FROM maintable WHERE name = ?";

        // Voorbereid de DTO lijst. Dit mag trouwens ook een Set zijn, wat je maar wil.
        // Een List is makkelijker sorteerbaar en verwerkbaar.
        List<Maintable> results = new ArrayList<Maintable>();

        try {
            connection = DriverManager.getConnection(url);
            preparedStatement = connection.prepareStatement(preparedSelectQuery);
            preparedStatement.setString(1, "testnaam");
            resultSet = preparedStatement.executeQuery();

            // Verwerk de resultaten in een DTO lijst.
            while (resultSet.next()) {
                Maintable maintable = new Maintable();
                maintable.setId(new Long(resultSet.getLong("id")));
                maintable.setName(resultSet.getString("name"));
                maintable.setValue(new Integer(resultSet.getInt("value")));
                results.add(maintable);
            }

            System.out.println("Uitvoeren van de SELECT query is gelukt.");
            System.out.println("Aantal gevonden resultaten: " + results.size());
        } catch (SQLException e) {
            System.err.println("Uitvoeren van de SELECT query is mislukt.");
            e.printStackTrace();
        } finally {
            SqlUtil.close(resultSet);
            SqlUtil.close(preparedStatement);
            SqlUtil.close(connection);
        }

        // Doorloop de DTO lijst.
        for (Maintable result : results) {
            System.out.println(result);
        }
    }

}

Laden van de driver is gelukt.
Uitvoeren van de SELECT query is gelukt.
Aantal gevonden resultaten: 3
[ID=2,Name=testnaam,Value=10]
[ID=3,Name=testnaam,Value=10]
[ID=4,Name=testnaam,Value=10]

Zo'n lijst met DTO's kun je dan verder gebruiken buiten de database-communicatie-laag van je applicatie.

Terug naar boven

Een universele DAO ontwerpen?

De lappen code hierboven zijn in principe erg basaal en veel ervan is hetzelfde. Het is de kunst om deze lappen code netjes te "refactoren" in aparte klassen en methoden, zodat geen enkel stukje code dubbel voorkomt. Deze klassen zouden dan tezamen dan een Data Access Layer vormen met een generieke DAO (Data Access Object) dat alle queries zou moeten kunnen afhandelen. We gaan hier in dit artikel niet verder op in, maar om een idee te geven staat hieronder een voorbeeld van een SELECT query met behulp van een uitgekiende DAO:

package test;

import net.balusc.dao.DaoException;
import net.balusc.dao.DaoSession;
import net.balusc.dao.DatabaseType;
import net.balusc.dto.DtoList;
import net.balusc.query.LoadQuery;
import net.balusc.testdata.Maintable;

public class Test {

    private static DaoSession daoSession = new DaoSession(DatabaseType.MYSQL);

    static {
        daoSession.setUrl("jdbc:mysql://localhost/javabase");
        daoSession.setUsername("root");
        daoSession.setPassword("d$7hF_r!9Y");
    }

    public static void main(String[] args) {

        Maintable example = new Maintable();
        example.setName("testnaam");
        LoadQuery<Maintable> loadQuery = new LoadQuery<Maintable>(example);
        
        try {
            daoSession.execute(loadQuery);
        } catch (DaoException e) {
            e.printStackTrace();
            return;
        }

        DtoList<Maintable> results = loadQuery.getOutput();

        for (Maintable result : results) {
            System.out.println(result);
        }
    }

}

[INFO] net.balusc.sql.PreparedQuery#executeSelectQuery: SELECT Maintable.ID AS Maintable_ID, Maintable.Name AS Maintable_Name, Maintable.Value AS Maintable_Value FROM Maintable WHERE Maintable.Name = ? [testnaam]
[ID=2,Name=testnaam,Value=10]
[ID=3,Name=testnaam,Value=10]
[ID=4,Name=testnaam,Value=10]

Een wat 'normalere' aanpak van een DAO kun je in deze Engelstalige DAO tutorial lezen: DAO tutorial - the data layer.

Er zijn ook gratis danwel commerciële ORM's (Object Relational Mappers) verkrijgbaar die het vanuit Java werken met databases moet vergemakkelijken. Het bekendste voorbeeld is wel Hibernate. Bij een ORM komt het zo ongeveer erop neer dat je de database objectmatig via diverse hulpklassen kunt benaderen zonder handgeschreven SQL queries. Hibernate heeft helaas echter wel een enorm hoge leercurve, met name het kennis van XML is vereist en het op de juiste wijze configureren van de XML configuratie bestanden kan nogal lastig worden. Maar het is beslist wel de moeite waard.

Terug naar boven

Copyright - Er is geen copyright op de code. Je kunt het naar believen overnemen, aanpassen danwel verspreiden.

(C) Maart 2007, BalusC