Sunday, December 27, 2009

Uploading files with JSF 2.0 and Servlet 3.0

WARNING - OUTDATED CONTENT!

This article is targeted on JSF 2.0/2.1. Since JSF 2.2 there's finally native file upload component in flavor of <h:inputFile> whose value can be tied to a javax.servlet.http.Part property. It's recommended to make use of it directly. See also this question & answer.

Introduction

The new Servlet 3.0 specification made uploading files really easy. However, because JSF 2.0 isn't initially designed to be primarily used on top of Servlet 3.0 and should be backwards compatible with Servlet 2.5, it lacks a standard file upload component. Until now you could have used among others Tomahawk's t:inputFileUpload for that. But as of now (December 2009) Tomahawk appears not to be "JSF 2.0 ready" yet and has problems here and there when being used on a JSF 2.0 environment. When you're targeting a Servlet 3.0 compatible container such as Glassfish v3, then you could also just create a custom JSF file upload component yourself.

To prepare, you need to have a Filter which puts the parts of a multipart/form-data request into the request parameter map before the FacesServlet kicks in. The FacesServlet namely doesn't have builtin facilities for this relies on the availablilty of the submitted input component values in the request parameter map. You can find it all here. Put the three classes MultipartMap, MultipartFilter and MultipartRequest in the classpath. The renderer of the custom file upload component relies on them in case of multipart/form-data requests.

Back to top

Custom component and renderer

With the new JSF 2.0 annotations it's now more easy to create custom components yourself. You don't need to hassle with somewhat opaque XML configurations anymore. I however only had a little hard time in figuring the best way to create custom components with help of annotations, because it's nowhere explained in the Java EE 6 tutorial nor the JSR314 - JSF 2.0 Specification. I am sure that the Sun JSF guys are also reading here, so here it is: Please work on that, it was already opaque in JSF 1.x and it should not be that more opaque in JSF 2.0!

At any way, I finally figured it with little help of Jim Driscoll's blog and exploring the JSF 2.0 source code.

First, let's look what we need: in the line of h:inputText component which renders a HTML input type="text" element, we would like to have a fictive h:inputFile component which renders a HTML input type="file" element. As h:inputText component is represented by a HtmlInputText class, we would thus like to have a HtmlInputFile class which extends HtmlInputText and overrides the renderer type so that it generates a HTML input type="file" element instead.

Okay, that's no big deal, so here it is:

/*
 * net/balusc/jsf/component/html/HtmlInputFile.java
 * 
 * Copyright (C) 2009 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.jsf.component.html;

import javax.faces.component.FacesComponent;
import javax.faces.component.html.HtmlInputText;

/**
 * Faces component for <code>input type="file"</code> field.
 * 
 * @author BalusC
 * @link http://balusc.blogspot.com/2009/12/uploading-files-with-jsf-20-and-servlet.html
 */
@FacesComponent(value = "HtmlInputFile")
public class HtmlInputFile extends HtmlInputText {

    // Getters ------------------------------------------------------------------------------------
    
    @Override
    public String getRendererType() {
        return "javax.faces.File";
    }

}

The nice thing is that this component inherits all of the standard attributes of HtmlInputText so that you don't need to redefine them (fortunately not; it would have been a fairly tedious task and a lot of code).

The value of the @FacesComponent annotation represents the component-type which is to be definied in the taglib xml file (shown later). The getRendererType() should return the renderer-type of the renderer class which is to be annotated using @FacesRenderer.

Extending the renderer is however quite a work when you want to be implementation independent, you need to take all possible attributes into account here as well. In this case we assume that you're going to use and stick to Mojarra 2.x forever (and thus not replace by another JSF implementation such as MyFaces sooner or later). Analogous with extending HtmlInputText to HtmlInputFile we thus want to extend its Mojarra-specific renderer TextRenderer to FileRenderer so that it renders a HTML input type="file" element instead.

/*
 * net/balusc/jsf/renderer/html/FileRenderer.java
 * 
 * Copyright (C) 2009 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.jsf.renderer.html;

import java.io.File;
import java.io.IOException;

import javax.faces.component.UIComponent;
import javax.faces.component.UIInput;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.convert.ConverterException;
import javax.faces.render.FacesRenderer;

import net.balusc.http.multipart.MultipartRequest;

import com.sun.faces.renderkit.Attribute;
import com.sun.faces.renderkit.AttributeManager;
import com.sun.faces.renderkit.RenderKitUtils;
import com.sun.faces.renderkit.html_basic.TextRenderer;

/**
 * Faces renderer for <code>input type="file"</code> field.
 * 
 * @author BalusC
 * @link http://balusc.blogspot.com/2009/12/uploading-files-with-jsf-20-and-servlet.html
 */
@FacesRenderer(componentFamily = "javax.faces.Input", rendererType = "javax.faces.File")
public class FileRenderer extends TextRenderer {

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

    private static final String EMPTY_STRING = "";
    private static final Attribute[] INPUT_ATTRIBUTES =
        AttributeManager.getAttributes(AttributeManager.Key.INPUTTEXT);

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

    @Override
    protected void getEndTextToRender
        (FacesContext context, UIComponent component, String currentValue)
            throws IOException
    {
        ResponseWriter writer = context.getResponseWriter();
        writer.startElement("input", component);
        writeIdAttributeIfNecessary(context, writer, component);
        writer.writeAttribute("type", "file", null);
        writer.writeAttribute("name", (component.getClientId(context)), "clientId");

        // Render styleClass, if any.
        String styleClass = (String) component.getAttributes().get("styleClass");
        if (styleClass != null) {
            writer.writeAttribute("class", styleClass, "styleClass");
        }

        // Render standard HTMLattributes expect of styleClass.
        RenderKitUtils.renderPassThruAttributes(
            context, writer, component, INPUT_ATTRIBUTES, getNonOnChangeBehaviors(component));
        RenderKitUtils.renderXHTMLStyleBooleanAttributes(writer, component);
        RenderKitUtils.renderOnchange(context, component, false);

        writer.endElement("input");
    }

    @Override
    public void decode(FacesContext context, UIComponent component) {
        rendererParamsNotNull(context, component);
        if (!shouldDecode(component)) {
            return;
        }
        String clientId = decodeBehaviors(context, component);
        if (clientId == null) {
            clientId = component.getClientId(context);
        }
        File file = ((MultipartRequest) context.getExternalContext().getRequest()).getFile(clientId);

        // If no file is specified, set empty String to trigger validators.
        ((UIInput) component).setSubmittedValue((file != null) ? file : EMPTY_STRING);
    }

    @Override
    public Object getConvertedValue(FacesContext context, UIComponent component, Object submittedValue)
        throws ConverterException
    {
        return (submittedValue != EMPTY_STRING) ? submittedValue : null;
    }

}

Note that the @FacesRenderer annotation also specifies a component family of "javax.faces.Input" and that this is nowhere specified in our HtmlInputFile. That's also not needed, it's already inherited from HtmlInputText.

Now, to use the custom JSF 2.0 file upload component in Facelets we really need to define another XML file. It's however not a big deal. You fortunately don't need to define all the tag attributes as you should have done in case of JSP. Just define the namespace (which you need to specify in the xmlns attribute of the <html> tag), the tag name (to identify the tag in XHTML) and the component type (as definied in the @FacesComponent of the associated component class).

Create a new XML file at /WEB-INF/balusc.taglib.xml and fill it as follows:

<?xml version="1.0" encoding="UTF-8"?>
<facelet-taglib
    xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
        http://java.sun.com/xml/ns/javaee/web-facelettaglibrary_2_0.xsd"
    version="2.0">

    <namespace>http://balusc.net/jsf/html</namespace>
    <tag>
        <tag-name>inputFile</tag-name>
        <component>
            <component-type>HtmlInputFile</component-type>
        </component>
    </tag>
</facelet-taglib>

You need to familarize Facelets with the new taglib in web.xml as follows:


    <context-param>
        <param-name>javax.faces.FACELETS_LIBRARIES</param-name>
        <param-value>/WEB-INF/balusc.taglib.xml</param-value>
    </context-param>

Note, if you have multiple Facelets taglibs, then you can separate the paths with a semicolon ;.

Back to top

Basic use example

Here is a basic use example of a JSF managed bean and a Facelets page which demonstrates the working of all of the stuff. First the managed bean UploadBean:

package net.balusc.example.upload;

import java.io.File;
import java.util.Arrays;

import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;

@ManagedBean
@RequestScoped
public class UploadBean {

    private String text;
    private File file;
    private String[] check;

    public void submit() {
        // Now do your thing with the obtained input.
        System.out.println("Text: " + text);
        System.out.println("File: " + file);
        System.out.println("Check: " + Arrays.toString(check));
    }

    public String getText() {
        return text;
    }

    public File getFile() {
        return file;
    }

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

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

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

    public void setCheck(String[] check) {
        this.check = check;
    }
    
}

And now the Facelets page upload.xhtml:

<!DOCTYPE html>
<html
    xmlns="http://www.w3.org/1999/xhtml"
    xmlns:f="http://java.sun.com/jsf/core"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:hh="http://balusc.net/jsf/html">
    <h:head>
        <title>JSF 2.0 and Servlet 3.0 file upload test</title>
        <style>label { float: left; display: block; width: 75px; }</style>
    </h:head>
    <h:body>
        <h:form id="form" method="post" enctype="multipart/form-data">
            <h:outputLabel for="text">Text:</h:outputLabel>
            <h:inputText id="text" value="#{uploadBean.text}" />
            <br />
            <h:outputLabel for="file">File:</h:outputLabel>
            <hh:inputFile id="file" value="#{uploadBean.file}" />
            <h:outputText value="File #{uploadBean.file.name} successfully uploaded!" 
                rendered="#{not empty uploadBean.file}" />
            <br />
            <h:selectManyCheckbox id="check" layout="pageDirection" value="#{uploadBean.check}">
                <f:selectItem itemLabel="Check 1:" itemValue="check1" />
                <f:selectItem itemLabel="Check 2:" itemValue="check2" />
            </h:selectManyCheckbox>
            <h:commandButton value="submit" action="#{uploadBean.submit}" />
            <h:messages />
        </h:form>
    </h:body>
</html>

Copy'n'paste the stuff and run it at http://localhost:8080/playground/upload.jsf (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 that you have the FacesServlet in web.xml mapped on *.jsf) and see it working! And no, you don't need to do anything with faces-config.xml, the managed bean is automagically found and initialized with help of the new JSF 2.0 annotations.

Note: this all is developed and tested with Eclipse 3.5 and Glassfish v3.

Back to top

Validate uploaded file

The lack of the support of @MultipartConfig annotation in the filter and JSF also implies that the size of the uploaded file can't be restricted by the maxFileSize annotation field. It is however possible to attach a simple validator to the custom component. Here's an example:

package net.balusc.example.upload;

import java.io.File;

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

@FacesValidator(value = "fileValidator")
public class FileValidator implements Validator {

    private static final long MAX_FILE_SIZE = 10485760L; // 10MB.
    
    @Override
    public void validate(FacesContext context, UIComponent component, Object value)
        throws ValidatorException
    {
        File file = (File) value;
        if (file != null && file.length() > MAX_FILE_SIZE) {
            file.delete(); // Free resources!
            throw new ValidatorException(new FacesMessage(String.format(
                "File exceeds maximum permitted size of %d bytes.", MAX_FILE_SIZE)));
        }
    }

}

You can attach it as follows:


    <hh:inputFile validator="fileValidator" />

You can also use a f:validator instead:


    <hh:inputFile>
        <f:validator validatorId="fileValidator" />
    </hh:inputFile>

That should be it. Also no faces-config stuff is needed here thanks to the annotations.

Back to top

Copyright - GNU Lesser General Public License

(C) December 2009, BalusC

Uploading files in Servlet 3.0

Warning

The MultipartFilter class does not work out the box in Tomcat versions older than 7.0.7!

In order to get it to run properly, you need at least Tomcat 7.0.7 and to set allowCasualMultipartParsing="true" in the webapp's <Context> element in Webapp/META-INF/context.xml or Tomcat/conf/server.xml. See also Tomcat 7 issue 47911.

Introduction

The new Servlet 3.0 specification includes among others support for parsing multipart/form-data requests. All you basically need to do is to annotate the Servlet with @MultipartConfig annotation. No need for Apache Commons FileUpload anymore! Interesting detail is however that both Oracle Glassfish v3 and Apache Tomcat 7.0 actually silently uses Apache Commons FileUpload under the covers to fulfill the new Servlet 3.0 feature!


@MultipartConfig
public class UploadServlet extends HttpServlet {}

This way all multipart/form-data parts are available by HttpServletRequest#getParts(). It returns a collection of Part elements. This is to be used instead of the normal getParameter() calls and so on. The Part API itself is however somewhat limited in the degree of abstraction. To find out whether the part represents a normal text field or a file field, you'll have to parse the content-disposition header yourself to find out if the filename parameter is in there. Also, when you want to get the actual parameter value as String, you need to read the Part#getInputStream() into a String yourself. You'll also have to collect multiple parameter values together yourself based on the part name, where you could have used getParameterValues().

All that extra work does not harm if you have only one file upload servlet in your webapplication. But at times you would like to avoid repeating the same code again and again. Or you would like to continue using the getParameter() stuff the same way as for normal request. Or you would like to have all the parts be available as HttpServletRequest#getParameterMap() in Expression Language as you did before by ${param}.

Back to top

MultipartMap

For that I've created the MultipartMap. It simulates the HttpServletRequest#getParameterXXX() methods to ease the processing in @MultipartConfig servlets. You can access the normal request parameters by getParameter() and you can access multiple request parameter values by getParameterValues().

On creation, the MultipartMap will put itself in the request scope, identified by the attribute name parts, so that you can access the parameters in EL by for example ${parts.fieldname} where you would have used ${param.fieldname}. In case of file fields, the ${parts.filefieldname} returns a File object.

It was a design decision to extend HashMap<String, Object> instead of having just Map<String, String[]> and Map<String, File> properties, because of the accessibility in Expression Language. Also, when the value is obtained by get(), as will happen in EL, then multiple parameter values will be converted from String[] to List<String>, so that you can use it in the JSTL fn:contains function.

/*
 * net/balusc/http/multipart/MultipartMap.java
 *
 * Copyright (C) 2009 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.http.multipart;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import javax.servlet.MultipartConfigElement;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.Part;

/**
 * The MultipartMap. It simulates the <code>HttpServletRequest#getParameterXXX()</code> methods to
 * ease the processing in <code>@MultipartConfig</code> servlets. You can access the normal request
 * parameters by <code>{@link #getParameter(String)}</code> and you can access multiple request
 * parameter values by <code>{@link #getParameterValues(String)}</code>.
 * <p>
 * On creation, the <code>MultipartMap</code> will put itself in the request scope, identified by
 * the attribute name <code>parts</code>, so that you can access the parameters in EL by for example
 * <code>${parts.fieldname}</code> where you would have used <code>${param.fieldname}</code>. In
 * case of file fields, the <code>${parts.filefieldname}</code> returns a <code>{@link File}</code>.
 * <p>
 * It was a design decision to extend <code>HashMap&lt;String, Object&gt;</code> instead of having
 * just <code>Map&lt;String, String[]&gt;</code> and <code>Map&lt;String, File&gt;</code>
 * properties, because of the accessibility in Expression Language. Also, when the value is obtained
 * by <code>{@link #get(Object)}</code>, as will happen in EL, then multiple parameter values will
 * be converted from <code>String[]</code> to <code>List&lt;String&gt;</code>, so that you can use
 * it in the JSTL <code>fn:contains</code> function.
 *
 * @author BalusC
 * @link http://balusc.blogspot.com/2009/12/uploading-files-in-servlet-30.html
 */
public class MultipartMap extends HashMap<String, Object> {

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

    private static final String ATTRIBUTE_NAME = "parts";
    private static final String CONTENT_DISPOSITION = "content-disposition";
    private static final String CONTENT_DISPOSITION_FILENAME = "filename";
    private static final String DEFAULT_ENCODING = "UTF-8";
    private static final int DEFAULT_BUFFER_SIZE = 10240; // 10KB.

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

    private String encoding;
    private String location;
    private boolean multipartConfigured;

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

    /**
     * Construct multipart map based on the given multipart request and the servlet associated with
     * the request. The file upload location will be extracted from <code>@MultipartConfig</code>
     * of the servlet. When the encoding is not specified in the given request, then it will default
     * to <tt>UTF-8</tt>.
     * @param multipartRequest The multipart request to construct the multipart map for.
     * @param servlet The servlet which is responsible for the given request.
     * @throws ServletException If something fails at Servlet level.
     * @throws IOException If something fails at I/O level.
     */
    public MultipartMap(HttpServletRequest multipartRequest, Servlet servlet)
        throws ServletException, IOException
    {
        this(multipartRequest, new MultipartConfigElement(
            servlet.getClass().getAnnotation(MultipartConfig.class)).getLocation(), true);
    }

    /**
     * Construct multipart map based on the given multipart request and file upload location. When
     * the encoding is not specified in the given request, then it will default to <tt>UTF-8</tt>.
     * @param multipartRequest The multipart request to construct the multipart map for.
     * @param location The location to save uploaded files in.
     * @throws ServletException If something fails at Servlet level.
     * @throws IOException If something fails at I/O level.
     */
    public MultipartMap(HttpServletRequest multipartRequest, String location)
        throws ServletException, IOException
    {
        this(multipartRequest, location, false);
    }

    /**
     * Global constructor.
     */
    private MultipartMap
        (HttpServletRequest multipartRequest, String location, boolean multipartConfigured)
            throws ServletException, IOException
    {
        multipartRequest.setAttribute(ATTRIBUTE_NAME, this);

        this.encoding = multipartRequest.getCharacterEncoding();
        if (this.encoding == null) {
            multipartRequest.setCharacterEncoding(this.encoding = DEFAULT_ENCODING);
        }
        this.location = location;
        this.multipartConfigured = multipartConfigured;

        for (Part part : multipartRequest.getParts()) {
            String filename = getFilename(part);
            if (filename == null) {
                processTextPart(part);
            } else if (!filename.isEmpty()) {
                processFilePart(part, filename);
            }
        }
    }

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

    @Override
    public Object get(Object key) {
        Object value = super.get(key);
        if (value instanceof String[]) {
            String[] values = (String[]) value;
            return values.length == 1 ? values[0] : Arrays.asList(values);
        } else {
            return value; // Can be File or null.
        }
    }

    /**
     * @see ServletRequest#getParameter(String)
     */
    public String getParameter(String name) {
        Object value = super.get(name);
        if (value instanceof File) {
            return ((File) value).getName();
        }
        String[] values = (String[]) value;
        return values != null ? values[0] : null;
    }

    /**
     * @see ServletRequest#getParameterValues(String)
     */
    public String[] getParameterValues(String name) {
        Object value = super.get(name);
        if (value instanceof File) {
            return new String[] { ((File) value).getName() };
        }
        return (String[]) value;
    }

    /**
     * @see ServletRequest#getParameterNames()
     */
    public Enumeration<String> getParameterNames() {
        return Collections.enumeration(keySet());
    }

    /**
     * @see ServletRequest#getParameterMap()
     */
    public Map<String, String[]> getParameterMap() {
        Map<String, String[]> map = new HashMap<String, String[]>();
        for (Entry<String, Object> entry : entrySet()) {
            Object value = entry.getValue();
            if (value instanceof String[]) {
                map.put(entry.getKey(), (String[]) value);
            } else {
                map.put(entry.getKey(), new String[] { ((File) value).getName() });
            }
        }
        return map;
    }

    /**
     * Returns uploaded file associated with given request parameter name.
     * @param name Request parameter name to return the associated uploaded file for.
     * @return Uploaded file associated with given request parameter name.
     * @throws IllegalArgumentException If this field is actually a Text field.
     */
    public File getFile(String name) {
        Object value = super.get(name);
        if (value instanceof String[]) {
            throw new IllegalArgumentException("This is a Text field. Use #getParameter() instead.");
        }
        return (File) value;
    }

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

    /**
     * Returns the filename from the content-disposition header of the given part.
     */
    private String getFilename(Part part) {
        for (String cd : part.getHeader(CONTENT_DISPOSITION).split(";")) {
            if (cd.trim().startsWith(CONTENT_DISPOSITION_FILENAME)) {
                return cd.substring(cd.indexOf('=') + 1).trim().replace("\"", "");
            }
        }
        return null;
    }

    /**
     * Returns the text value of the given part.
     */
    private String getValue(Part part) throws IOException {
        BufferedReader reader = 
            new BufferedReader(new InputStreamReader(part.getInputStream(), encoding));
        StringBuilder value = new StringBuilder();
        char[] buffer = new char[DEFAULT_BUFFER_SIZE];
        for (int length = 0; (length = reader.read(buffer)) > 0;) {
            value.append(buffer, 0, length);
        }
        return value.toString();
    }

    /**
     * Process given part as Text part.
     */
    private void processTextPart(Part part) throws IOException {
        String name = part.getName();
        String[] values = (String[]) super.get(name);

        if (values == null) {
            // Not in parameter map yet, so add as new value.
            put(name, new String[] { getValue(part) });
        } 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] = getValue(part);
            put(name, newValues);
        }
    }

    /**
     * Process given part as File part which is to be saved in temp dir with the given filename.
     */
    private void processFilePart(Part part, String filename) throws IOException {
        // First fix stupid MSIE behaviour (it passes full client side path along filename).
        filename = filename
            .substring(filename.lastIndexOf('/') + 1)
            .substring(filename.lastIndexOf('\\') + 1);

        // Get filename prefix (actual name) and suffix (extension).
        String prefix = filename;
        String suffix = "";
        if (filename.contains(".")) {
            prefix = filename.substring(0, filename.lastIndexOf('.'));
            suffix = filename.substring(filename.lastIndexOf('.'));
        }

        // Write uploaded file.
        File file = File.createTempFile(prefix + "_", suffix, new File(location));
        if (multipartConfigured) {
            part.write(file.getName()); // Will be written to the very same File.
        } else {
            InputStream input = null;
            OutputStream output = null;
            try {
                input = new BufferedInputStream(part.getInputStream(), DEFAULT_BUFFER_SIZE);
                output = new BufferedOutputStream(new FileOutputStream(file), DEFAULT_BUFFER_SIZE);
                byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
                for (int length = 0; ((length = input.read(buffer)) > 0);) {
                    output.write(buffer, 0, length);
                }
            } finally {
                if (output != null) try { output.close(); } catch (IOException logOrIgnore) { /**/ }
                if (input != null) try { input.close(); } catch (IOException logOrIgnore) { /**/ }
            }
        }

        put(part.getName(), file);
        part.delete(); // Cleanup temporary storage.
    }

}

It is necessary to know the file upload location in the MultipartMap as well, because we can then make use of File#createTempFile() to create files with an unique filename to avoid them being overwritten by another files with a (by coincidence) same name. Once you have the uploaded file at hands in the servlet or bean, you can always make use of File#renameTo() to do a fast rename/move.

Back to top

Basic use example

Here is a basic use example of a servlet and JSP file which demonstrates the working of the MultipartMap.

package net.balusc.example.upload;

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

import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import net.balusc.http.multipart.MultipartMap;

@WebServlet(urlPatterns = { "/upload" })
@MultipartConfig(location = "/upload", maxFileSize = 10485760L) // 10MB.
public class UploadServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException
    {
        request.getRequestDispatcher("/WEB-INF/upload.jsp").forward(request, response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException
    {
        MultipartMap map = new MultipartMap(request, this);
        String text = map.getParameter("text");
        File file = map.getFile("file");
        String[] check = map.getParameterValues("check");

        // Now do your thing with the obtained input.
        System.out.println("Text: " + text);
        System.out.println("File: " + file);
        System.out.println("Check: " + Arrays.toString(check));

        request.getRequestDispatcher("/WEB-INF/upload.jsp").forward(request, response);
    }

}

That was the UploadServlet. Note the two annotations. The @WebServlet annotation definies under each the url-pattern, the URL pattern on which the servlet should listen. The @MultipartConfig annotation defines the location at the local disk file system where uploaded files are to be stored. In this case it is the /upload folder. In Windows environments with the application server running on the C:/ disk, this location effectively points to C:/upload. Ensure that you have created this folder beforehand!

Here's the JSP file, the /WEB-INF/upload.jsp:

<%@ page pageEncoding="UTF-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>

<!doctype html>
<html lang="en">
    <head>
        <title>Servlet 3.0 file upload test</title>
        <style>label { float: left; display: block; width: 75px; }</style>
    </head>
    <body>
        <form action="upload" method="post" enctype="multipart/form-data">
            <label for="text">Text:</label>
            <input type="text" id="text" name="text" value="${parts.text}">
            <br>
            <label for="file">File:</label>
            <input type="file" id="file" name="file">
            <c:if test="${not empty parts.file}">
                File ${parts.file.name} successfully uploaded!
            </c:if>
            <br>
            <label for="check1">Check 1:</label>
            <input type="checkbox" id="check1" name="check" value="check1"
                ${fn:contains(parts.check, 'check1') ? 'checked' : ''}>
            <br>
            <label for="check2">Check 2:</label>
            <input type="checkbox" id="check2" name="check" value="check2"
                ${fn:contains(parts.check, 'check2') ? 'checked' : ''}>
            <br>
            <input type="submit" value="submit">
        </form>
    </body>
</html>

Copy'n'paste the stuff and run it at http://localhost:8080/playground/upload (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 working! And no, you don't need to declare the servlet in web.xml, the servlets are automagically loaded and initialized with help of the new Servlet 3.0 annotations.

Note: this all is developed and tested with Eclipse 3.5 and Glassfish v3.

Back to top

More abstraction

As you might have noticed, the MultipartMap class here above has a second public constructor taking the file upload location as String parameter instead of the involved servlet. This is useful in circumstances where you'd like to abstract the entire HttpServletRequest, including the parameter map, away with help of a Filter and a HttpServletRequestWrapper. This way you can just access the request parameters the unchanged EL way by ${param}. This is also useful if you're running a MVC framework on top of the Servlet API which doesn't support the @MultipartConfig annotation, such as JSF 2.0 (here's an article about uploading files in JSF 2.0 + Servlet 3.0). The use of @MultipartConfig annotation is restricted to servlets only, so with a filter you need to specify the file upload location yourself, hence the second constructor of MultipartMap.

Here's the Filter which could be used to process multipart/form-data request transparently:

/*
 * net/balusc/http/multipart/MultipartFilter.java
 * 
 * Copyright (C) 2009 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.http.multipart;

import java.io.IOException;

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.annotation.WebFilter;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.http.HttpServletRequest;

/**
 * This filter detects <tt>multipart/form-data</tt> and <tt>multipart/mixed</tt> POST requests and
 * will then replace the <code>HttpServletRequest</code> by a <code>{@link MultipartRequest}</code>.
 * 
 * @author BalusC
 * @link http://balusc.blogspot.com/2009/12/uploading-files-in-servlet-30.html
 */

@WebFilter(urlPatterns = { "/*" }, initParams = {
    @WebInitParam(name = "location", value = "/upload") })
public class MultipartFilter implements Filter {

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

    private static final String INIT_PARAM_LOCATION = "location";
    private static final String REQUEST_METHOD_POST = "POST";
    private static final String CONTENT_TYPE_MULTIPART = "multipart/";

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

    private String location;

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

    @Override
    public void init(FilterConfig config) throws ServletException {
        this.location = config.getInitParameter(INIT_PARAM_LOCATION);
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        if (isMultipartRequest(httpRequest)) {
            request = new MultipartRequest(httpRequest, location);
        }
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
        // NOOP.
    }

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

    /**
     * Returns true if the given request is a multipart request.
     * @param request The request to be checked.
     * @return True if the given request is a multipart request.
     */
    public static final boolean isMultipartRequest(HttpServletRequest request) {
        return REQUEST_METHOD_POST.equalsIgnoreCase(request.getMethod())
            && request.getContentType() != null
            && request.getContentType().toLowerCase().startsWith(CONTENT_TYPE_MULTIPART);
    }

}

It is true that the location property is a bit nonsensicial since it is already "hardcoded" by an annotation in the very same filter class. It is however overrideable by a real init param in web.xml!

And now the MultipartRequest which the filter needs to replace the request with:

/*
 * net/balusc/http/multipart/MultipartRequest.java
 * 
 * Copyright (C) 2009 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.http.multipart;

import java.io.File;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.Part;

/**
 * This class represents a multipart request. It not only abstracts the <code>{@link Part}</code>
 * away, but it also provides direct access to the <code>{@link MultipartMap}</code>, so that one 
 * can get the uploaded files out of it.
 * 
 * @author BalusC
 * @link http://balusc.blogspot.com/2009/12/uploading-files-in-servlet-30.html
 */
public class MultipartRequest extends HttpServletRequestWrapper {

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

    private MultipartMap multipartMap;

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

    /**
     * Construct MultipartRequest based on the given HttpServletRequest.
     * @param request HttpServletRequest to be wrapped into a MultipartRequest.
     * @param location The location to save uploaded files in.
     * @throws IOException If something fails at I/O level.
     * @throws ServletException If something fails at Servlet level.
     */
    public MultipartRequest(HttpServletRequest request, String location)
        throws ServletException, IOException
    {
        super(request);
        this.multipartMap = new MultipartMap(request, location);
    }

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

    @Override
    public String getParameter(String name) {
        return multipartMap.getParameter(name);
    }

    @Override
    public String[] getParameterValues(String name) {
        return multipartMap.getParameterValues(name);
    }

    @Override
    public Enumeration<String> getParameterNames() {
        return multipartMap.getParameterNames();
    }

    @Override
    public Map<String, String[]> getParameterMap() {
        return multipartMap.getParameterMap();
    }

    /**
     * @see MultipartMap#getFile(String)
     */
    public File getFile(String name) {
        return multipartMap.getFile(name);
    }

}

That should be it. And no, also no web.xml modifications are needed here. The web.xml is pretty superflous with the new Servlet 3.0 annotations.

When the Filter is in use, then the first lines of UploadServlet#doPost() can now be changed as follows:


    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException
    {
        String text = request.getParameter("text");
        File file = ((MultipartRequest) request).getFile("file");
        String[] check = request.getParameterValues("check");
        ...
    }

This also implies that the @MultipartConfig annotation can be removed from the servlet. You only need to handle file size limits yourself, but that can now be done more nicely (it would by default abort the entire request and show a HTTP 500 error page otherwise, not very good for User eXperience). The ${parts} in the EL throughout the JSP file can also be changed back to the normal ${param}, including the ones for the uploaded files.

Back to top

Copyright - GNU Lesser General Public License

(C) December 2009, BalusC

Thursday, September 10, 2009

Webapplication performance tips and tricks

Introduction

Yahoo has a great performance analysis tool in flavor of a Firefox addon: YSlow (yes, you need to install the -also great- Firebug addon first). The YSlow site has already explained all of the best practices in detail here.

Yahoo's explanations are in general clear enough for the average Java EE web application developer, but when the YSlow's Server category comes into the picture, Yahoo unfortunately only gives examples based on Apache HTTP server and PHP and in a few cases also IIS. In this article I'll "translate" the relevant subcategories into the Java EE approach based on Apache Tomcat 6.0. As a bonus, a few more best practices are added and explained in detail.

Back to top

Use a Content Delivery Network

This is the first rule of the YSlow's Server category. Well, the idea is nice, but this is in my opinion not a "must". Having a secondary domain (no, not a subdomain) for pure static content is a more general practice to gain performance in serving static content. A webbrowser is namely restricted to have a certain maximum amount of simultaneous open connections on a single domain. In the older browser versions this is usually limited to 2 and ranges nowadays around 10-15 connections. This can also be changed using a simple regedit (MSIE) or by editing about:config (Firefox). Those kind of tweaks are usually only done by the more advanced users with an above average knowledge of the software they use.

So, to give a broader area of visitors a better performance experience, it may be better to have a secondary domain for pure static content only. E.g. onedomain.com for JSP files and anotherdomain.com for CSS/JS/Flash/etc files. Or of course such a CDN as suggested by Yahoo, but again, a CDN for private static data is in my opinion a bit nonsensicial. After all, if you respect the performance rules for static content the correct way, then the static content will actually only be requested whenever really needed, so this makes a secondary domain or CDN more superfluous. Or you must have a webapplication which needs to serve a lot of non-layout-related images, such as photography.

For 3rd party public static content it's however definitely worth the effort to link it to a CDN which is provided by themselves, if any. For example jQuery offers several CDN hosts. It's a win-win situation for both your server and the client.

Back to top

Add an Expires or a Cache-Control Header

This is the second rule of the YSlow's Server category. A very good point. The Expires header prevents the browser to re-request the same static content (JS/CSS/images/etc) everytime, which is only a waste of the available time, connections and bandwidth. When you're serving static content from public webcontent in Tomcat, then the DefaultServlet is responsible for serving the content. It unfortunately does nothing with the Expires header. Although it supports the Last-Modified headers, this costs effectively a HEAD request which is already one connection and request too much when the content is actually not changed after all. You can however override the DefaultServlet with an own implementation as outlined here. How to do it effectively is already covered by the earlier FileServlet article at this blog. This servlet is a well suited solution for the second, third as well as the fourth rule of the YSlow's Server category.

About the cache-control header for dynamic content, the general practice is that we just want to avoid caching of dynamic content, especially the pages containing forms or the pages in restricted area. You can do that by adding the following response headers to the base controller Servlet or Filter of your webapplication:


    ...

    response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); // HTTP 1.1.
    response.setHeader("Pragma", "no-cache"); // HTTP 1.0.
    response.setDateHeader("Expires", 0); // Proxies.

    ...

There is a little story behind the no-store and must-revalidate attributes of the cache-control header: some webbrowsers (including Firefox) doesn't cache the page when those attributes are omitted! According to the HTTP specification only the no-cache should have been sufficient. But OK, now we at least have the 'magic' three headers which should work for all decent webbrowsers and proxies.

Back to top

Use Query String with a timestamp to force re-request

The Expires header is useful, but .. with a (too) far-future Expires header, the client won't check for any updates on the static resource anymore until the expire date has passed, or you clear the browser cache, or you do a hard-refresh (CTRL+F5)! A common practice is then to append an unique query string to the URL of the static content denoting a timestamp of the last file modification or the server startup time, so that the browser is forced to re-request it whenever the query string changes.

Determining the last modification time on every request is more expensive than just determining the server startup time only once in application's lifetime. It is generally sufficient to do so. Whenever the server restarts, the browser will send a HEAD request to check if there are any updates. Assuming that your server doesn't restart every minute or so, this doesn't harm that much. Here's an example of how to do it using a ServletContextListener:

package mypackage;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

/**
 * Configure the webapplication context. This is to be placed in the application scope.
 * As far now this example only sets the startup time.
 * @author BalusC
 * @see http://balusc.blogspot.com/2009/09/webapplication-performance-tips-and.html
 */
public class Config implements ServletContextListener {

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

    private static final String CONFIG_ATTRIBUTE_NAME = "config";

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

    private long startupTime;

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

    /**
     * Obtain startup time and put Config itself in the application scope.
     * @see ServletContextListener#contextInitialized(ServletContextEvent)
     */
   public void contextInitialized(ServletContextEvent event) {
        this.startupTime = System.currentTimeMillis() / 1000;
        event.getServletContext().setAttribute(CONFIG_ATTRIBUTE_NAME, this);
    }

    /**
     * @see ServletContextListener#contextDestroyed(ServletContextEvent)
     */
    public void contextDestroyed(ServletContextEvent event) {
        // Nothing to do here.
    }

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

    /**
     * Returns the startup time associated with this configuration.
     * @return The startup time associated with this configuration.
     */
    public long getStartupTime() {
        return this.startupTime;
    }

}

Just add it as a listener to the web.xml the usual way:


    ...

    <listener>
        <listener-class>mypackage.Config</listener-class>
    </listener>

    ...

Here is an example of how to use it in JSP:


        ...

        <link rel="stylesheet" type="text/css" href="/static/style.css?${config.startupTime}">
        <script type="text/javascript" src="/static/script.js?${config.startupTime}"></script>

        ...

As a side-note, if you're using the aforementioned FileServlet as well, then you can in theory postpone the default expire time more. For example 1 year (365 days):


    ...

    private static final long DEFAULT_EXPIRE_TIME = 31536000000L; // ..ms = 365 days.

    ...

Back to top

Add LastModified timestamp to CSS background images

Appending query string with a timestamp to static CSS files is nice, but .. this doesn't cover the CSS background images! Those counts each as a separate request. If you don't append a timestamp query string to them, then they won't be checked for any updates. How to handle it may differ per environment, so I'll only describe my general approach to give the idea. You might need to finetune it further to suit your environment. I myself use a batch job using YUI Compressor (yes, it's a Java API!) to minify all CSS and JS files before deploy. After getting the minified result, regexp is used to find all background images in the CSS source and File#lastModified() is used to get the last modification timestamp from it and finally the originals will be replaced. Here's a basic example of the Minifier -keep in mind, this may needed to be modified to suit your environment:

package mypackage;

import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.yahoo.platform.yui.compressor.CssCompressor;

/**
 * The Minifier.
 * @author BalusC
 * @see http://balusc.blogspot.com/2009/09/webapplication-performance-tips-and.html
 */
public class Minifier {

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

    /**
     * Minify all CSS files on given basePath + cssPath to the given basePath + minPath and append
     * lastmodified timestamps to CSS background images relative to the given basePath.
     * @param basePath The base path of static content.
     * @param cssPath The path of all CSS files, relative to the given basePath.
     * @param minPath The path of all minified CSS files, relative to the given basePath.
     * @throws IOException If something fails at I/O level.
     */
    public static void minifyCss(String basePath, String cssPath, String minPath) throws IOException {
        for (File cssFile : new File(basePath + cssPath).listFiles()) {
            if (cssFile.isFile()) {
                File minFile = new File(basePath + minPath, cssFile.getName());
                minifyCss(basePath, cssFile, minFile);
            }
        }
    }

    /**
     * Minify given cssFile to the given minFile and append lastmodified timestamps to CSS
     * background images relative to the given basePath.
     * @param basePath The base path of static content.
     * @param cssFile The CSS file to be minified.
     * @param minFile The minified CSS file.
     * @throws IOException If something fails at I/O level.
     */
    public static void minifyCss(String basePath, File cssFile, File minFile) throws IOException {
        Reader reader = null;
        Writer writer = null;

        try {
            // Read original CSS file.
            reader = new InputStreamReader(new FileInputStream(cssFile), "UTF-8");

            // Minify original CSS file.
            StringWriter stringWriter = new StringWriter();
            new CssCompressor(reader).compress(stringWriter, -1);
            String line = stringWriter.toString();

            // Find all CSS background images.
            Matcher matcher = Pattern.compile("url\\([\'\"]?([/\\w\\.]*)[\'\"]?\\)").matcher(line);
            Set<String> imagePaths = new HashSet<String>();
            while (matcher.find()) {
                imagePaths.add(matcher.group(1));
            }

            // Append lastmodified timestamps to CSS background images and replace originals.
            for (String imagePath : imagePaths) {
                long lastModified = new File(basePath + imagePath).lastModified() / 1000;
                line = line.replace(imagePath, imagePath + "?" + lastModified);
            }

            // Write minified CSS file.
            writer = new OutputStreamWriter(new FileOutputStream(minFile), "UTF-8");
            writer.write(line);
        } finally {
            close(writer);
            close(reader);
        }

        // Dumb sysout, replace by Logger if needed ;)
        System.out.println("Minifying " + cssFile + " to " + minFile + " succeed!");
    }

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

    /**
     * Silently close given resource. Any IOException will be printed to stdout.
     * This global method can easily be extracted to your "IOUtil" class, if not already exist.
     * @param resource Resource to be closed.
     */
    private static void close(Closeable resource) {
        if (resource != null) {
            try {
                resource.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    // Main method --------------------------------------------------------------------------------
    
    /**
     * Just to demonstrate how your batch job thing should use the Minifier.
     */
    public static void main(String... args) throws Exception {
        String basePath = "C:/Workspace/YourProject/WebContent/WEB-INF";
        String cssPath = "/static/css";
        String minPath = cssPath + "/min";
        Minifier.minifyCss(basePath, cssPath, minPath);
    }
    
}
Back to top

Gzip Components

This is the third rule of the YSlow's Server category. Yes, that's also a very good point. Gzip is relatively fast and can save up to 70% of the network bandwidth. For static text content you can just use the aforementioned FileServlet article at this blog. For dynamic text content you'll need to configure the application server so that it uses GZIP compression. This is usually explained in the documentation of the application server in question. In case of Apache Tomcat 6.0 you can find it here. You need to extend the <Connector> element in Tomcat/conf/server.xml with a compression attribute which is set to "on". Here's a basic example (note the last attribute):


    ...

    <Connector
        protocol="HTTP/1.1"
        port="80"
        redirectPort="8443"
        connectionTimeout="20000"
        compression="on" />

    ...

That's all! Restart Tomcat and all dynamic response will be Gzipped. And no, this does not affect the aforementioned FileServlet for static content, you can just keep it as is.

Back to top

Configure ETags

This is the fourth rule of the YSlow's Server category. Again a good point and again also covered by the aforementioned FileServlet article at this blog. The ETags are not needed for dynamic content as they are usually not to be cached.

Back to top

Flush the Buffer Early

This is the fifth rule of the YSlow's Server category. Well, that's also a good point. Flushing the response between </head> and <body>. But that's one of the 0,01% cases where in you can't quickly go around a (cough) scriptlet and thus its use is less or more forgiveable.


        ...

    </head>
    <% response.flushBuffer(); %>
    <body>

        ...

However, in case of Apache Tomcat 6.0 the HTTP connector uses a buffer size of 2KB (2048 bytes) by default which is configureable using the bufferSize attribute. This is generally more than good enough. The average HTML head with the "default" minimum tags (doctype, html, head, meta content type, meta description, base, favicon, CSS file, JS file and title) already accounts 1 up to 1.5KB in size. In any way, in one of my last webapps I have used a slightly modified WhitespaceFilter which removes all whitespace inside the <body> and instantly pre-flushes the stream before the <body>.

Back to top

Use NIO

When your webapplication needs to handle more than around 1.000 concurrent connections, or when your webserver is also used for other purposes than only serving the web, then it's generally better to use non-blocking IO streams instead of blocking IO streams. It scales much better as you don't need one implicitly opened thread per opened IO resource anymore, instead basically all resources are managed by a single thread. This saves the server from a lot of threads and the overhead of controlling them and the exponentially growing performance drop when the amount of concurrent threads (HTTP connections) gets high. You're for performance also not dependent on the amount of available threads anymore, but more on the amount of available heap memory. It can go up to around 20.000 concurrent connections on a single thread instead of around 5.000 concurrent connections on that much threads.

Most decent servers supports NIO, as does Apache Tomcat 6.0 in the HTTP connector. Basically all you need to do is to replace the default protocol attribute of "HTTP/1.1" with "org.apache.coyote.http11.Http11NioProtocol". The Tomcat NIO connector implementation is also known as "Grizzly". In some full fledged Java EE application servers like Sun Glassfish, this is by default turned on.


    ...

    <Connector
        protocol="org.apache.coyote.http11.Http11NioProtocol"
        port="80"
        redirectPort="8443"
        connectionTimeout="20000"
        compression="on" />

    ...

That's basically all! Restart Tomcat and now it will use NIO to handle HTTP connections. Only ensure that you give it enough memory (also in the IDE when developing with it). You can start with 512MB, but 1024MB is better.

Back to top

Copyright - No text of this article may be taken over without explicit authorisation. Only the code is free of copyright. You can copy, change and distribute the code freely. Just mentioning this site should be fair.

(C) September 2009, BalusC