Wednesday, December 19, 2007

Action dependent requireness

Introduction

There are scenarios where you want to depend the "requireness" (between quotes, this word doesn't occur in an English dictionary) of an UIInput element on the UICommand action invoked. There are also a lot of hacks and workarounds written about it, e.g. using immediate="true" and valueChangeListener or binding attributes to retain the value (which gives you only two scenario cases) or moving the validation to the backing bean actions (which gives you endless scenario cases), etcetera.

Those workarounds require extra logic in the backing bean. It would be great if JSF offers an default possibility to let the requireness of the UIInput depend on the UICommand action invoked. Having an attribute like requiredActions which accepts a commaseparated string of the ID's of the buttons would be great. Unfortunately such an attribute isn't available. But don't feel disappointed, there is a possibility to use the request parameter map to determine which UICommand action invoked. Its client ID is namely available in the request parameter map. So just checking the presence of the client ID in the #{param} ought to be enough. KISS! ;)

Back to top

Basic JSF example

Here is a sample form. It represents a login form with two input fields and two buttons. The username as well as the password fields are required when you press the "Login" button, while the password field isn't required when you press the "Forget password" button. This is done by checking the presence of the client ID of the "Login" button in the required attribute of the password field. You can also check the absence of the client ID of the "Forget password" button as well.

The stuff is tested in a Java EE 5.0 environment with Tomcat 6.0 with Servlet 2.5, JSP 2.1 and JSF 1.2_07 (currently called Mojarra).

<h:form id="form">
    <h:panelGrid columns="3">
        <h:outputLabel for="username" value="Username" />
        <h:inputText id="username" value="#{myBean.username}"
            required="true" />
        <h:message for="username" style="color: red;" />

        <h:outputLabel for="password" value="Password" />
        <h:inputSecret id="password" value="#{myBean.password}"
            required="#{!empty param['form:login']}" />
        <h:message for="password" style="color: red;" />

        <h:panelGroup />
        <h:panelGroup>
            <h:commandButton id="login" value="Login" action="#{myBean.login}" />
            <h:commandButton id="forget" value="Forget password" action="#{myBean.forget}" />
        </h:panelGroup>
        <h:message for="form" style="color: green;" />
    </h:panelGrid>
</h:form>

The appropriate test backing bean (request scoped) look like:

package mypackage;

import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;

public class MyBean {

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

    private String username;
    private String password;

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

    public void login() {

        // Just for debug. Don't do this in real! Hash the password, compare in DB and forget it ;)
        System.out.println("Login username: " + username);
        System.out.println("Login password: " + password);

        // Show succes message.
        FacesContext.getCurrentInstance().addMessage("form", new FacesMessage("Login succesful!"));
    }

    public void forget() {

        // Just for debug.
        System.out.println("Forget username: " + username);

        // Show succes message.
        FacesContext.getCurrentInstance().addMessage("form", new FacesMessage("New password sent!"));
    }

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

    public String getUsername() {
        return username;
    }

    public String getPassword() {
        return password;
    }

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

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

}

Now, when you hit the "Login" button, it will validate the username as well as the password fields, but when you hit the "Forget password" button, it will validate the username field only!

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) December 2007, BalusC

Friday, December 14, 2007

Set focus and highlight in JSF

Notice

This article is targeted on JSF 1.2. Whilst this can be used in JSF 2.0 as well, a better JSF 2.0 implementation is provided by OmniFaces with its <o:highlight> component. See also showcase example here.


The power of a PhaseListener

This article shows how to use a PhaseListener to set focus to the first input element which has a FacesMessage (which can be caused by a validation or conversion error or any other custom reason) and highlight all elements which has a FacesMessage. It is relatively simple, it costs effectively only a few lines inside the beforePhase of the RENDER_RESPONSE, two small Javascript functions for the focus and highlight and a single CSS class for the highlight.

Here is a sample form. Note the Javascript which should be placed at the very end of the HTML body, at least after the input element which should be focused and highlighted. The stuff is tested in a Java EE 5.0 environment with Tomcat 6.0 with Servlet 2.5, JSP 2.1 and JSF 1.2_07 (currently called Mojarra).

<h:form id="form">
    <h:panelGrid columns="3">
        <h:outputLabel for="input1" value="Enter input 1" />
        <h:inputText id="input1" value="#{myBean.input1}" required="true" />
        <h:message for="input1" style="color: red;" />

        <h:outputLabel for="input2" value="Enter input 2" />
        <h:inputText id="input2" value="#{myBean.input2}" required="true" />
        <h:message for="input2" style="color: red;" />

        <h:outputLabel for="input3" value="Enter input 3" />
        <h:inputText id="input3" value="#{myBean.input3}" required="true" />
        <h:message for="input3" style="color: red;" />

        <h:panelGroup />
        <h:commandButton value="Submit" action="#{myBean.doSomething}" />
        <h:panelGroup />
    </h:panelGrid>
</h:form>

<script>
    setHighlight('${highlight}');
    setFocus('${focus}');
</script>

This is how the Javascript functions setFocus() and setHighlight() should look like:

/**
 * Set focus on the element of the given id.
 * @param id The id of the element to set focus on.
 */
function setFocus(id) {
    var element = document.getElementById(id);
    if (element && element.focus) {
        element.focus();
    }
}

/**
 * Set highlight on the elements of the given ids. It basically sets the classname of the elements
 * to 'highlight'. This require at least a CSS style class '.highlight'.
 * @param ids The ids of the elements to be highlighted, comma separated.
 */
function setHighlight(ids) {
    var idsArray = ids.split(",");
    for (var i = 0; i < idsArray.length; i++) {
        var element = document.getElementById(idsArray[i]);
        if (element) {
            element.className = 'highlight';
        }
    }
}

And now the CSS style class for the highlight:

.highlight {
    background-color: #fcc;
}

And finally the PhaseListener which sets the focus to the first input element which has a FacesMessage and highlights all input elements which has a FacesMessage:

/*
 * net/balusc/webapp/SetFocusListener.java
 * 
 * Copyright (C) 2007 BalusC
 * 
 * This program is free software: you can redistribute it and/or modify it under the terms of the
 * GNU Lesser General Public License as published by the Free Software Foundation, either version 3
 * of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with this library.
 * If not, see <http://www.gnu.org/licenses/>.
 */

package net.balusc.webapp;

import java.util.Iterator;

import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;

/**
 * This phase listener checks if there is a client ID with message and will set the client ID as 
 * ${focus} in the request map. It will also gather all client IDs with message and will set it as
 * ${highlight} in the request map.
 * <p>
 * This phase listener should be configured in the faces-config.xml as follows:
 * <pre>
 * &lt;lifecycle&gt;
 *     &lt;phase-listener&gt;net.balusc.webapp.SetFocusListener&lt;/phase-listener&gt;
 * &lt;/lifecycle&gt;
 * </pre>
 * 
 * @author BalusC
 * @link http://balusc.blogspot.com/2007/12/set-focus-in-jsf.html
 */
public class SetFocusListener implements PhaseListener {

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

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

        // Listen on render response phase.
        return PhaseId.RENDER_RESPONSE;
    }

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

        // Init.
        FacesContext facesContext = event.getFacesContext();
        String focus = null;
        StringBuilder highlight = new StringBuilder();

        // Iterate over all client ID's with messages.
        Iterator<String> clientIdsWithMessages = facesContext.getClientIdsWithMessages();
        while (clientIdsWithMessages.hasNext()) {
            String clientIdWithMessages = clientIdsWithMessages.next();
            if (focus == null) {
                focus = clientIdWithMessages;
            }
            highlight.append(clientIdWithMessages);
            if (clientIdsWithMessages.hasNext()) {
                highlight.append(",");
            }
        }

        // Set ${focus} and ${highlight} in JSP.
        facesContext.getExternalContext().getRequestMap().put("focus", focus);
        facesContext.getExternalContext().getRequestMap().put("highlight", highlight.toString());
    }

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

}

Define the SetFocusListener as follows in the faces-config.xml:

    <lifecycle>
        <phase-listener>net.balusc.webapp.SetFocusListener</phase-listener>
    </lifecycle>

That's all, folks!

Back to top

Copyright - GNU Lesser General Public License

(C) December 2007, BalusC

Tuesday, December 11, 2007

Validator for multiple fields

Notice

The JSF utility library OmniFaces has several useful multi-field validators such as <o:validateEqual>, which may end up to be easier than homegrowing one. See also the javadoc of the base class ValidateMultipleFields.

Introduction

Validators in JSF are nice. They, however, have its shortcomings. They will by default validate only one field at once. There is no standard way to attach one validator to multiple fields. Although there are some situations where you want this kind of functionality. For example validating the password confirmation field, validating the range of two numeric or date values (e.g. the one have to be lesser than the other), validating correctness of the three separate day, month and year fields, etcetera.

The cleanest solution would be to create a custom component which renders two or more components and use a specific validator for that, but that would involve more work. The easiest solution in this particular case is to attach the validator to the first component of the group (components are rendered, validated, converted and updated in the same order as you define them in the JSF view) and pass the other component(s) as unique f:attribute facet(s) along the first component. Then in the validator you can get the desired component(s) using UIComponent#getAttributes().

Back to top

Basic example

This example demonstrates a basic registration form with one username field and two password fields. The value of the second password field should equal to the value of the first password field before the action method may be invoked. The stuff is tested in a Java EE 5.0 environment with Tomcat 6.0 with Servlet 2.5, JSP 2.1 and JSF 1.2_07 (currently called Mojarra by the way!).

Here is the relevant JSF code. Note the binding="#{confirm}" of the second password field. It binds the component to the view and makes it available elsewhere by #{confirm}. This is an instance of the UIInput class which has a getSubmittedValue() method to get the submitted value. Note the f:attribute of the first password field, its value should point to the component of the second password field #{confirm}. Also note that the second password field doesn't have any value bound to the backing bean as this is unnecessary in this specific case.

<h:form id="register">
    <h:panelGrid columns="3">
        <h:outputLabel for="username" value="Username" />
        <h:inputText id="username" value="#{myBean.username}" required="true" />
        <h:message for="username" style="color: red;" />

        <h:outputLabel for="password" value="Password" />
        <h:inputSecret id="password" value="#{myBean.password}" required="true">
            <f:validator validatorId="passwordValidator" />
            <f:attribute name="confirm" value="#{confirm}" />
        </h:inputSecret>
        <h:message for="password" style="color: red;" />

        <h:outputLabel for="confirm" value="Confirm password" />
        <h:inputSecret id="confirm" binding="#{confirm}" required="true" />
        <h:message for="confirm" style="color: red;" />

        <h:panelGroup />
        <h:commandButton value="Register" action="#{myBean.register}" />
        <h:message for="register" style="color: green;" />
    </h:panelGrid>
</h:form>

And now the validator code:

package mypackage;

import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.component.UIInput;
import javax.faces.context.FacesContext;
import javax.faces.validator.Validator;
import javax.faces.validator.ValidatorException;

public class PasswordValidator implements Validator {

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

    public void validate(FacesContext context, UIComponent component, Object value)
        throws ValidatorException
    {
        // Cast the value of the entered password to String.
        String password = (String) value;

        // Obtain the component and submitted value of the confirm password component.
        UIInput confirmComponent = (UIInput) component.getAttributes().get("confirm");
        String confirm = confirmComponent.getSubmittedValue();

        // Check if they both are filled in.
        if (password == null || password.isEmpty() || confirm == null || confirm.isEmpty()) {
            return; // Let required="true" do its job.
        }

        // Compare the password with the confirm password.
        if (!password.equals(confirm)) {
            confirmComponent.setValid(false); // So that it's marked invalid.
            throw new ValidatorException(new FacesMessage("Passwords are not equal."));
        }

        // You can even validate the minimum password length here and throw accordingly.
        // Or, if you're smart, calculate the password strength and throw accordingly ;)
    }

}

The appropriate test backing bean look like:

package mypackage;

import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;

public class MyBean {

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

    private String username;
    private String password;

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

    public void register() {

        // Just for debug. Don't do this in real! Hash the password, save to DB and forget it ;)
        System.out.println("Username: " + username);
        System.out.println("Password: " + password);

        // Show succes message.
        FacesContext.getCurrentInstance().addMessage("register", new FacesMessage("Succes!"));
    }

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

    public String getUsername() {
        return username;
    }

    public String getPassword() {
        return password;
    }

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

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

}

Finally the relevant part of the faces-config.xml:


    <validator>
        <validator-id>passwordValidator</validator-id>
        <validator-class>mypackage.PasswordValidator</validator-class>
    </validator>
    
    <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-bean>

That's all, folks!

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) December 2007, BalusC

Saturday, December 1, 2007

WhitespaceFilter

Whitespace

Whitespace is used everywhere. It covers spaces, tabs and newlines. It is used to distinguish lexical tokens from each other and also to keep the source code readable for the developer. But in case of HTML over network, whitespace costs bandwidth and therefore in some circumstances also money and/or performance. If you care about the bandwidth usage and/or the money and/or performance, then you can consider to trim off all whitespace of the HTML response. The only con is that it makes the HTML source code at the client side almost unreadable.

You can trim whitespace right in the HTML files (or JSP or JSF or whatever view you're using, as long as it writes plain HTML response), but that would make the source code unreadable for yourself. Better way is to use a Filter which trims the whitespace from the response.

Back to top

Replace response writer

Here is how such a WhitespaceFilter can look like. It is relatively easy, it actually replaces the writer of the HttpServletResponse with a customized implementation of PrintWriter. This implemetation will trim whitespace off from any strings and character arrays before writing it to the response stream. It also take care of any <pre> and <textarea> tags and keep the whitespace of its contents unchanged. However it doesn't care about the CSS white-space: pre; property, because it would involve too much work to check on that (parse HTML, lookup CSS classes, sniff the appropriate style and parse it again, etc). It isn't worth that effort. Just use <pre> tags if you want to use preformatted text ;)

Note that this filter only works on requests which are passed through a servlet which writes the response to the PrintWriter, e.g. JSP and JSF files (parsed by JspServlet and FacesServlet respectively) or custom servlets which uses HttpServletResponse#getWriter() to write output. This filter does not work on requests for plain vanilla CSS, Javascript, HTML files and images and another binary files which aren't written through the PrintWriter, but through the OutputStream. If you want to implement the same thing for the OutputStream, then you'll have to check the content type first if it starts with "text" or not, otherwise binary files would be screwed up. Unfortunately in real (at least, in Tomcat 6.0) the content type is set after the output stream is acquired, thus we cannot determine the content type during acquiring the output stream.

The stuff is tested in a Java EE 5.0 environment with Tomcat 6.0 with Servlet 2.5, JSP 2.1, JSTL 1.2 and JSF 1.2_06.

/*
 * net/balusc/webapp/WhitespaceFilter.java
 * 
 * Copyright (C) 2007 BalusC
 * 
 * This program is free software; you can redistribute it and/or modify it under the terms of the
 * GNU General Public License as published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with this program; if
 * not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 */

package net.balusc.webapp;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StringReader;

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.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;

/**
 * This filter class removes any whitespace from the response. It actually trims all leading and 
 * trailing spaces or tabs and newlines before writing to the response stream. This will greatly
 * save the network bandwith, but this will make the source of the response more hard to read.
 * <p>
 * This filter should be configured in the web.xml as follows:
 * <pre>
 * &lt;filter&gt;
 *     &lt;description&gt;
 *         This filter class removes any whitespace from the response. It actually trims all
 *         leading and trailing spaces or tabs and newlines before writing to the response stream.
 *         This will greatly save the network bandwith, but this will make the source of the
 *         response more hard to read.
 *     &lt;/description&gt;
 *     &lt;filter-name&gt;whitespaceFilter&lt;/filter-name&gt;
 *     &lt;filter-class&gt;net.balusc.webapp.WhitespaceFilter&lt;/filter-class&gt;
 * &lt;/filter&gt;
 * &lt;filter-mapping&gt;
 *     &lt;filter-name&gt;whitespaceFilter&lt;/filter-name&gt;
 *     &lt;url-pattern&gt;/*&lt;/url-pattern&gt;
 * &lt;/filter-mapping&gt;
 * </pre>
 *
 * @author BalusC
 * @link http://balusc.blogspot.com/2007/12/whitespacefilter.html
 */
public class WhitespaceFilter implements Filter {

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

    // Specify here where you'd like to start/stop the trimming.
    // You may want to replace this by init-param and initialize in init() instead.
    static final String[] START_TRIM_AFTER = {"<html", "</textarea", "</pre"};
    static final String[] STOP_TRIM_AFTER = {"</html", "<textarea", "<pre"};

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

    /**
     * @see Filter#init(FilterConfig)
     */
    public void init(FilterConfig config) throws ServletException {
        //
    }

    /**
     * @see Filter#doFilter(ServletRequest, ServletResponse, FilterChain)
     */
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException
    {
        if (response instanceof HttpServletResponse) {
            HttpServletResponse httpres = (HttpServletResponse) response;
            chain.doFilter(request, wrapResponse(httpres, createTrimWriter(httpres)));
        } else {
            chain.doFilter(request, response);
        }
    }

    /**
     * @see Filter#destroy()
     */
    public void destroy() {
        //
    }

    // Utility (may be refactored to public utility class) ----------------------------------------

    /**
     * Create a new PrintWriter for the given HttpServletResponse which trims all whitespace.
     * @param response The involved HttpServletResponse.
     * @return A PrintWriter which trims all whitespace.
     * @throws IOException If something fails at I/O level.
     */
    private static PrintWriter createTrimWriter(final HttpServletResponse response)
        throws IOException
    {
        return new PrintWriter(new OutputStreamWriter(response.getOutputStream(), "UTF-8"), true) {
            private StringBuilder builder = new StringBuilder();
            private boolean trim = false;

            public void write(int c) {
                builder.append((char) c); // It is actually a char, not an int.
            }

            public void write(char[] chars, int offset, int length) {
                builder.append(chars, offset, length);
                this.flush(); // Preflush it.
            }

            public void write(String string, int offset, int length) {
                builder.append(string, offset, length);
                this.flush(); // Preflush it.
            }

            // Finally override the flush method so that it trims whitespace.
            public void flush() {
                synchronized (builder) {
                    BufferedReader reader = new BufferedReader(new StringReader(builder.toString()));
                    String line = null;

                    try {
                        while ((line = reader.readLine()) != null) {
                            if (startTrim(line)) {
                                trim = true;
                                out.write(line);
                            } else if (trim) {
                                out.write(line.trim());
                                if (stopTrim(line)) {
                                    trim = false;
                                    println();
                                }
                            } else {
                                out.write(line);
                                println();
                            }
                        }
                    } catch (IOException e) {
                        setError();
                        // Log e or do e.printStackTrace() if necessary.
                    }

                    // Reset the local StringBuilder and issue real flush.
                    builder = new StringBuilder();
                    super.flush();
                }
            }

            private boolean startTrim(String line) {
                for (String match : START_TRIM_AFTER) {
                    if (line.contains(match)) {
                        return true;
                    }
                }
                return false;
            }

            private boolean stopTrim(String line) {
                for (String match : STOP_TRIM_AFTER) {
                    if (line.contains(match)) {
                        return true;
                    }
                }
                return false;
            }
        };
    }

    /**
     * Wrap the given HttpServletResponse with the given PrintWriter.
     * @param response The HttpServletResponse of which the given PrintWriter have to be wrapped in.
     * @param writer The PrintWriter to be wrapped in the given HttpServletResponse.
     * @return The HttpServletResponse with the PrintWriter wrapped in.
     */
    private static HttpServletResponse wrapResponse(
        final HttpServletResponse response, final PrintWriter writer)
    {
        return new HttpServletResponseWrapper(response) {
            public PrintWriter getWriter() throws IOException {
                return writer;
            }
        };
    }

}

WhitespaceFilter configuration in web.xml:


    <filter>
        <description>
            This filter class removes any whitespace from the response. It actually trims all
            leading and trailing spaces or tabs and newlines before writing to the response stream.
            This will greatly save the network bandwith, but this will make the source of the
            response more hard to read.
        </description>
        <filter-name>whitespaceFilter</filter-name>
        <filter-class>net.balusc.webapp.WhitespaceFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>whitespaceFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

That's all, folks!

Back to top

Copyright - GNU General Public License

(C) December 2007, BalusC

Thursday, November 8, 2007

MultipartFilter

Upload and store files

Update: if you're using Servlet 3.0 or newer, then there are built-in ways to process file uploads. You may find it more useful: Uploading files in Servlet 3.0.

Downloading files is made relatively easy using a FileServlet, but uploading files is a bit harder. Entering/selecting the raw absolute file path in input type="text" and sending it to the server so that it can be used in a File object isn't going to work, as the server doesn't have access to the client's file system. That will work only if the server as well as the client runs on the same machine and that wouldn't occur in real life.

To browse and select a file for upload you need a input type="file" field in the form. As stated in the HTML specification you have to use the POST method and the enctype attribute of the form have to be set to "multipart/form-data".


<form action="myServlet" method="post" enctype="multipart/form-data">
    <input type="file" name="file" />
    <input type="submit" />
</form>

After submitting such a form the binary multipart form data is available in the HttpServletRequest#getInputStream(). For testing purposes you can read the stream using the following snippet:


BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));
String line = null;
while ((line = reader.readLine()) != null) {
    System.out.println(line);
}

Parsing such a stream requires precise background knowledge of how multipart form data requests are structured. The standard Servlet API namely doesn't provide builtin facilities to parse them. The form fields aren't available as parameter of the request (e.g. request.getParameter("name") will always return null), they are included in the binary stream. The uploaded files are also included in the binary stream. To create a perfect multipart parser you'll have to write a lot of code. But don't feel disappointed, there are lot of 3rd party multipart parsers available. A commonly used one is the Apache Commons FileUpload. It can parse the multipart form data into several FileItem objects (misleading class name by the way, I'd rather call it MultipartItem). You'll have to filter the parameters and files out yourself.

Back to top

MultipartFilter

To save you the effort, I've played around with it a while and wrote a MultipartFilter which automatically detects if the request is a multipart form data request, parses the request and store the parameters back in the parameter map of HttpServletRequest and stores the uploaded files as attributes of the HttpServletRequest. This way you can just continue writing the Servlet logic as usual.

It makes use of the Apache Commons FileUpload API 1.2. So you need at least the following JAR's (newer versions are allowed) in the classpath, e.g. in /WEB-INF/lib:

Here is the code. The stuff is tested in a Java EE 5.0 environment with Tomcat 6.0 with Servlet 2.5, JSP 2.1 and JSTL 1.2.

/*
 * net/balusc/webapp/MultipartFilter.java
 * 
 * Copyright (C) 2007 BalusC
 * 
 * This program is free software: you can redistribute it and/or modify it under the terms of the
 * GNU Lesser General Public License as published by the Free Software Foundation, either version 3
 * of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with this library.
 * If not, see <http://www.gnu.org/licenses/>.
 */

package net.balusc.webapp;

import java.io.IOException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

/**
 * Check for multipart HttpServletRequests and parse the multipart form data so that all regular
 * form fields are available in the parameterMap of the HttpServletRequest and that all form file
 * fields are available as attribute of the HttpServletRequest. The attribute value of a form file
 * field can be an instance of FileItem or FileUploadException.
 * <p>
 * This filter requires at least the following JAR's (newer versions are allowed) in the classpath,
 * e.g. in /WEB-INF/lib.
 * <ul>
 * <li>commons-fileupload-1.2.jar</li>
 * <li>commons-io-1.3.2.jar</li>
 * </ul>
 * <p>
 * This filter should be definied as follows in the web.xml:
 * <pre>
 * &lt;filter&gt;
 *     &lt;description&gt;
 *         Check for multipart HttpServletRequests and parse the multipart form data so that all
 *         regular form fields are available in the parameterMap of the HttpServletRequest and that
 *         all form file fields are available as attribute of the HttpServletRequest. The attribute
 *         value of a form file field can be an instance of FileItem or FileUploadException.
 *     &lt;/description&gt;
 *     &lt;filter-name&gt;multipartFilter&lt;/filter-name&gt;
 *     &lt;filter-class&gt;net.balusc.webapp.MultipartFilter&lt;/filter-class&gt;
 *     &lt;init-param&gt;
 *         &lt;description&gt;
 *             Sets the maximum file size of the uploaded file in bytes. Set to 0 to indicate an
 *             unlimited file size. The example value of 1048576 indicates a maximum file size of
 *             1MB. This parameter is not required and can be removed safely.
 *         &lt;/description&gt;
 *         &lt;param-name&gt;maxFileSize&lt;/param-name&gt;
 *         &lt;param-value&gt;1048576&lt;/param-value&gt;
 *     &lt;/init-param&gt;
 * &lt;/filter&gt;
 * &lt;filter-mapping&gt;
 *     &lt;filter-name&gt;multipartFilter&lt;/filter-name&gt;
 *     &lt;url-pattern&gt;/*&lt;/url-pattern&gt;
 * &lt;/filter-mapping&gt;
 * </pre>
 *
 * @author BalusC
 * @link http://balusc.blogspot.com/2007/11/multipartfilter.html
 */
public class MultipartFilter implements Filter {

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

    private long maxFileSize;

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

    /**
     * Configure the 'maxFileSize' parameter.
     * @throws ServletException If 'maxFileSize' parameter value is not numeric.
     * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
     */
    public void init(FilterConfig filterConfig) throws ServletException {
        // Configure maxFileSize.
        String maxFileSize = filterConfig.getInitParameter("maxFileSize");
        if (maxFileSize != null) {
            if (!maxFileSize.matches("^\\d+$")) {
                throw new ServletException("MultipartFilter 'maxFileSize' is not numeric.");
            }
            this.maxFileSize = Long.parseLong(maxFileSize);
        }
    }

    /**
     * Check the type request and if it is a HttpServletRequest, then parse the request.
     * @throws ServletException If parsing of the given HttpServletRequest fails.
     * @see javax.servlet.Filter#doFilter(
     *      javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)
     */
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws ServletException, IOException
    {
        // Check type request.
        if (request instanceof HttpServletRequest) {
            // Cast back to HttpServletRequest.
            HttpServletRequest httpRequest = (HttpServletRequest) request;

            // Parse HttpServletRequest.
            HttpServletRequest parsedRequest = parseRequest(httpRequest);

            // Continue with filter chain.
            chain.doFilter(parsedRequest, response);
        } else {
            // Not a HttpServletRequest.
            chain.doFilter(request, response);
        }
    }

    /**
     * @see javax.servlet.Filter#destroy()
     */
    public void destroy() {
        // I am a boring method.
    }

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

    /**
     * Parse the given HttpServletRequest. If the request is a multipart request, then all multipart
     * request items will be processed, else the request will be returned unchanged. During the
     * processing of all multipart request items, the name and value of each regular form field will
     * be added to the parameterMap of the HttpServletRequest. The name and File object of each form
     * file field will be added as attribute of the given HttpServletRequest. If a
     * FileUploadException has occurred when the file size has exceeded the maximum file size, then
     * the FileUploadException will be added as attribute value instead of the FileItem object.
     * @param request The HttpServletRequest to be checked and parsed as multipart request.
     * @return The parsed HttpServletRequest.
     * @throws ServletException If parsing of the given HttpServletRequest fails.
     */
    @SuppressWarnings("unchecked") // ServletFileUpload#parseRequest() does not return generic type.
    private HttpServletRequest parseRequest(HttpServletRequest request) throws ServletException {

        // Check if the request is actually a multipart/form-data request.
        if (!ServletFileUpload.isMultipartContent(request)) {
            // If not, then return the request unchanged.
            return request;
        }

        // Prepare the multipart request items.
        // I'd rather call the "FileItem" class "MultipartItem" instead or so. What a stupid name ;)
        List<FileItem> multipartItems = null;

        try {
            // Parse the multipart request items.
            multipartItems = new ServletFileUpload(new DiskFileItemFactory()).parseRequest(request);
            // Note: we could use ServletFileUpload#setFileSizeMax() here, but that would throw a
            // FileUploadException immediately without processing the other fields. So we're
            // checking the file size only if the items are already parsed. See processFileField().
        } catch (FileUploadException e) {
            throw new ServletException("Cannot parse multipart request: " + e.getMessage());
        }

        // Prepare the request parameter map.
        Map<String, String[]> parameterMap = new HashMap<String, String[]>();

        // Loop through multipart request items.
        for (FileItem multipartItem : multipartItems) {
            if (multipartItem.isFormField()) {
                // Process regular form field (input type="text|radio|checkbox|etc", select, etc).
                processFormField(multipartItem, parameterMap);
            } else {
                // Process form file field (input type="file").
                processFileField(multipartItem, request);
            }
        }

        // Wrap the request with the parameter map which we just created and return it.
        return wrapRequest(request, parameterMap);
    }

    /**
     * Process multipart request item as regular form field. The name and value of each regular
     * form field will be added to the given parameterMap.
     * @param formField The form field to be processed.
     * @param parameterMap The parameterMap to be used for the HttpServletRequest.
     */
    private void processFormField(FileItem formField, Map<String, String[]> parameterMap) {
        String name = formField.getFieldName();
        String value = formField.getString();
        String[] values = parameterMap.get(name);

        if (values == null) {
            // Not in parameter map yet, so add as new value.
            parameterMap.put(name, new String[] { value });
        } else {
            // Multiple field values, so add new value to existing array.
            int length = values.length;
            String[] newValues = new String[length + 1];
            System.arraycopy(values, 0, newValues, 0, length);
            newValues[length] = value;
            parameterMap.put(name, newValues);
        }
    }

    /**
     * Process multipart request item as file field. The name and FileItem object of each file field
     * will be added as attribute of the given HttpServletRequest. If a FileUploadException has
     * occurred when the file size has exceeded the maximum file size, then the FileUploadException
     * will be added as attribute value instead of the FileItem object.
     * @param fileField The file field to be processed.
     * @param request The involved HttpServletRequest.
     */
    private void processFileField(FileItem fileField, HttpServletRequest request) {
        if (fileField.getName().length() <= 0) {
            // No file uploaded.
            request.setAttribute(fileField.getFieldName(), null);
        } else if (maxFileSize > 0 && fileField.getSize() > maxFileSize) {
            // File size exceeds maximum file size.
            request.setAttribute(fileField.getFieldName(), new FileUploadException(
                "File size exceeds maximum file size of " + maxFileSize + " bytes."));
            // Immediately delete temporary file to free up memory and/or disk space.
            fileField.delete();
        } else {
            // File uploaded with good size.
            request.setAttribute(fileField.getFieldName(), fileField);
        }
    }

    // Utility (may be refactored to public utility class) ----------------------------------------

    /**
     * Wrap the given HttpServletRequest with the given parameterMap.
     * @param request The HttpServletRequest of which the given parameterMap have to be wrapped in.
     * @param parameterMap The parameterMap to be wrapped in the given HttpServletRequest.
     * @return The HttpServletRequest with the parameterMap wrapped in.
     */
    private static HttpServletRequest wrapRequest(
        HttpServletRequest request, final Map<String, String[]> parameterMap)
    {
        return new HttpServletRequestWrapper(request) {
            public Map<String, String[]> getParameterMap() {
                return parameterMap;
            }
            public String[] getParameterValues(String name) {
                return parameterMap.get(name);
            }
            public String getParameter(String name) {
                String[] params = getParameterValues(name);
                return params != null && params.length > 0 ? params[0] : null;
            }
            public Enumeration<String> getParameterNames() {
                return Collections.enumeration(parameterMap.keySet());
            }
        };
    }

}

Add and configure the filter as follows in the web.xml:

<filter>
    <description>
        Check for multipart HttpServletRequests and parse the multipart form data so that all
        regular form fields are available in the parameterMap of the HttpServletRequest and that
        all form file fields are available as attribute of the HttpServletRequest. The attribute
        value of a form file field can be an instance of FileItem or FileUploadException.
    </description>
    <filter-name>multipartFilter</filter-name>
    <filter-class>net.balusc.webapp.MultipartFilter</filter-class>
    <init-param>
        <description>
            Sets the maximum file size of the uploaded file in bytes. Set to 0 to indicate an
            unlimited file size. The example value of 1048576 indicates a maximum file size of
            1MB. This parameter is not required and can be removed safely.
        </description>
        <param-name>maxFileSize</param-name>
        <param-value>1048576</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>multipartFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

That's all, folks!

Back to top

Basic use example

Here is a basic use example of a servlet, a form and JSP file which demonstrates the working of the MultipartFilter. Thanks to the MultipartFilter you can just use HttpServletRequest#getParameter() and #getParameterValues() for regular form fields. The uploaded file is available by HttpServletRequest#getAttribute(). If it is an instance of FileItem, then the upload was succesful, else if it is an instance of FileUploadException, then the upload was failed. The only cause can be that the file size exceeded the configured maximum file size.

package mypackage;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.io.FilenameUtils;

public class MyServlet extends HttpServlet {

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

    private File uploadFilePath;

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

    public void init() throws ServletException {
        // Configure uploadFilePath.
        String uploadFilePathParam = getServletConfig().getInitParameter("uploadFilePath");
        if (uploadFilePathParam == null) {
            throw new ServletException("MyServlet 'uploadFilePath' is not configured.");
        }
        uploadFilePath = new File(uploadFilePathParam);
        if (!uploadFilePath.exists()) {
            throw new ServletException("MyServlet 'uploadFilePath' does not exist.");
        }
        if (!uploadFilePath.isDirectory()) {
            throw new ServletException("MyServlet 'uploadFilePath' is not a directory.");
        }
        if (!uploadFilePath.canWrite()) {
            throw new ServletException("MyServlet 'uploadFilePath' is not writeable.");
        }
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException
    {
        // Do nothing, just show the form.
        forward(request, response);
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException
    {
        // Prepare bean.
        MyForm myForm = new MyForm();

        // Process request.
        process(request, myForm);

        // Store bean in request.
        request.setAttribute("myForm", myForm);

        // Postback.
        forward(request, response);
    }

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

    private void process(HttpServletRequest request, MyForm myForm) {
        // Validate text.
        String text = request.getParameter("text");
        if (isEmpty(text)) {
            // No text entered.
            myForm.setError("text", "Please enter some text.");
        }

        // Validate file.
        Object fileObject = request.getAttribute("file");
        if (fileObject == null) {
            // No file uploaded.
            myForm.setError("file", "Please select file to upload.");
        } else if (fileObject instanceof FileUploadException) {
            // File upload is failed.
            FileUploadException fileUploadException = (FileUploadException) fileObject;
            myForm.setError("file", fileUploadException.getMessage());
        }

        // Validate checkboxes.
        String[] check = request.getParameterValues("check");
        if (isEmpty(check)) {
            // No checkboxes checked.
            myForm.setError("check", "Please check one or more checkboxes.");
        }

        // If there are no errors, proceed with writing file.
        if (!myForm.hasErrors()) {
            FileItem fileItem = (FileItem) fileObject;

            // Get file name from uploaded file and trim path from it.
            // Some browsers (e.g. IE, Opera) also sends the path, which is completely irrelevant.
            String fileName = FilenameUtils.getName(fileItem.getName());

            // Prepare filename prefix and suffix for an unique filename in upload folder.
            String prefix = FilenameUtils.getBaseName(fileName) + "_";
            String suffix = "." + FilenameUtils.getExtension(fileName);

            try {
                // Prepare unique local file based on file name of uploaded file.
                File file = File.createTempFile(prefix, suffix, uploadFilePath);

                // Write uploaded file to local file.
                fileItem.write(file);

                // Set the file in form so that it can be provided for download.
                myForm.setFile(file);
            } catch (Exception e) {
                // Can be thrown by uniqueFile() and FileItem#write().
                myForm.setError("file", e.getMessage());
                e.printStackTrace();
            }
        }

        // If there are no errors after writing file, proceed with showing messages.
        if (!myForm.hasErrors()) {
            myForm.setMessage("text", "You have entered: " + text + ".");
            myForm.setMessage("file", "File succesfully uploaded.");
            myForm.setMessage("check", "You have checked: " + Arrays.toString(check) + ".");
        }
    }

    private void forward(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException
    {
        request.getRequestDispatcher("myForm.jsp").forward(request, response);
    }

    // Utilities (should be refactored to public utility classes) ---------------------------------

    /**
     * Check if the given object is empty. Returns true if the object is null, or if it is an
     * instance of String and its trimmed length is zero, or if it is an instance of an ordinary
     * array and its length is zero, or if it is an instance of Collection and its size is zero,
     * or if it is an instance of Map and its size is zero, or if its String representation is
     * null or the trimmed length of its String representation is zero.
     * @param value The object to be determined on emptiness.
     * @return True if the given object value is empty.
     */
    public static boolean isEmpty(Object value) {
        if (value == null) {
            return true;
        } else if (value instanceof String) {
            return ((String) value).trim().length() == 0;
        } else if (value instanceof Object[]) {
            return ((Object[]) value).length == 0;
        } else if (value instanceof Collection<?>) {
            return ((Collection<?>) value).size() == 0;
        } else if (value instanceof Map<?, ?>) {
            return ((Map<?, ?>) value).size() == 0;
        } else {
            return value.toString() == null || value.toString().trim().length() == 0;
        }
    }

}

Add and configure the servlet as follows in the web.xml:

<servlet>
    <servlet-name>myServlet</servlet-name>
    <servlet-class>mypackage.MyServlet</servlet-class>
    <init-param>
        <description>
            Set the file path where uploaded files should be stored in. This parameter is
            required.
        </description>
        <param-name>uploadFilePath</param-name>
        <param-value>c:/upload</param-value>
    </init-param>
</servlet>
<servlet-mapping>
    <servlet-name>myServlet</servlet-name>
    <url-pattern>/myServlet</url-pattern>
</servlet-mapping>

The form bean:

package mypackage;

import java.io.File;
import java.util.HashMap;
import java.util.Map;

public class MyForm {

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

    private String text;
    private File file;
    private String[] check;
    private Map<String, Boolean> checked = new HashMap<String, Boolean>();
    private Map<String, String> errors = new HashMap<String, String>();
    private Map<String, String> messages = new HashMap<String, String>();

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

    public String getText() {
        return text;
    }

    public File getFile() {
        return file;
    }

    public String[] getCheck() {
        return check;
    }

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

    public void setText(String text) {
        this.text = text;
    }

    public void setFile(File file) {
        this.file = file;
    }

    public void setCheck(String[] check) {
        checked = new HashMap<String, Boolean>();
        for (String value : check) {
            checked.put(value, Boolean.TRUE);
        }
        this.check = check;
    }

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

    public Map<String, Boolean> getChecked() {
        return checked;
    }

    public Map<String, String> getErrors() {
        return errors;
    }

    public Map<String, String> getMessages() {
        return messages;
    }

    public void setError(String fieldName, String message) {
        errors.put(fieldName, message);
    }

    public void setMessage(String fieldName, String message) {
        messages.put(fieldName, message);
    }

    public boolean hasErrors() {
        return errors.size() > 0;
    }

}

Finally the JSP file, save it as myForm.jsp in the root of the WebContent:

<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

<!doctype html>

<html lang="en">
    <head>
        <title>Test</title>
    </head>
    <body>
        <jsp:useBean id="myForm" class="mypackage.MyForm" scope="request" />
        <jsp:setProperty name="myForm" property="*" />

        <form action="myServlet" method="post" enctype="multipart/form-data">
            <label for="text" >Text:</label>
            <input type="text" id="text" name="text" value="${myForm.text}">
            <c:if test="${myForm.errors.text != null}">
                <span style="color: red;">${myForm.errors.text}</span>
            </c:if>
            <c:if test="${myForm.messages.text != null}">
                <span style="color: green;">${myForm.messages.text}</span>
            </c:if>
            <br>

            <label for="file" >File:</label>
            <input type="file" id="file" name="file">
            <c:if test="${myForm.errors.file != null}">
                <span style="color: red;">${myForm.errors.file}</span>
            </c:if>
            <c:if test="${myForm.messages.file != null}">
                <span style="color: green;">${myForm.messages.file}
                    <c:if test="${myForm.file != null}">
                        &nbsp;<a href="file/${myForm.file.name}">Download back</a>.
                    </c:if>
                </span>
            </c:if>
            <br>

            <label for="check1" >Check 1:</label>
            <input type="checkbox" id="check1" name="check" value="check1"
                ${myForm.checked.check1 ? 'checked' : ''}>
            <c:if test="${myForm.errors.check != null}">
                <span style="color: red;">${myForm.errors.check}</span>
            </c:if>
            <c:if test="${myForm.messages.check != null}">
                <span style="color: green;">${myForm.messages.check}</span>
            </c:if>
            <br>

            <label for="check2" >Check 2:</label>
            <input type="checkbox" id="check2" name="check" value="check2"
                ${myForm.checked.check2 ? 'checked' : ''} /></td>
            <br>

            <input type="submit">
        </form>
    </body>
</html>

Note: the download link makes use of the FileServlet. Make sure that it points to the same directory as where the file is uploaded. You can configure it as an init-param.

Copy'n'paste the stuff, run it on http://localhost:8080/playground/myServlet (assuming that your local development server runs at port 8080 and that the context root of your playground web application project is called 'playground') and see it wonderfully working!

Back to top

Copyright - GNU Lesser General Public License

(C) November 2007, BalusC