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}.
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.
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;
public class MultipartMap extends HashMap<String, Object> {
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;
private String encoding;
private String location;
private boolean multipartConfigured;
public MultipartMap(HttpServletRequest multipartRequest, Servlet servlet)
throws ServletException, IOException
{
this(multipartRequest, new MultipartConfigElement(
servlet.getClass().getAnnotation(MultipartConfig.class)).getLocation(), true);
}
public MultipartMap(HttpServletRequest multipartRequest, String location)
throws ServletException, IOException
{
this(multipartRequest, location, false);
}
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);
}
}
}
@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;
}
}
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;
}
public String[] getParameterValues(String name) {
Object value = super.get(name);
if (value instanceof File) {
return new String[] { ((File) value).getName() };
}
return (String[]) value;
}
public Enumeration<String> getParameterNames() {
return Collections.enumeration(keySet());
}
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;
}
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;
}
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;
}
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();
}
private void processTextPart(Part part) throws IOException {
String name = part.getName();
String[] values = (String[]) super.get(name);
if (values == null) {
put(name, new String[] { getValue(part) });
} else {
int length = values.length;
String[] newValues = new String[length + 1];
System.arraycopy(values, 0, newValues, 0, length);
newValues[length] = getValue(part);
put(name, newValues);
}
}
private void processFilePart(Part part, String filename) throws IOException {
filename = filename
.substring(filename.lastIndexOf('/') + 1)
.substring(filename.lastIndexOf('\\') + 1);
String prefix = filename;
String suffix = "";
if (filename.contains(".")) {
prefix = filename.substring(0, filename.lastIndexOf('.'));
suffix = filename.substring(filename.lastIndexOf('.'));
}
File file = File.createTempFile(prefix + "_", suffix, new File(location));
if (multipartConfigured) {
part.write(file.getName());
} 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();
}
}
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.
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)
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");
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.
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:
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;
@WebFilter(urlPatterns = { "/*" }, initParams = {
@WebInitParam(name = "location", value = "/upload") })
public class MultipartFilter implements Filter {
private static final String INIT_PARAM_LOCATION = "location";
private static final String REQUEST_METHOD_POST = "POST";
private static final String CONTENT_TYPE_MULTIPART = "multipart/";
private String location;
@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() {
}
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:
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;
public class MultipartRequest extends HttpServletRequestWrapper {
private MultipartMap multipartMap;
public MultipartRequest(HttpServletRequest request, String location)
throws ServletException, IOException
{
super(request);
this.multipartMap = new MultipartMap(request, location);
}
@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();
}
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.
Copyright - GNU Lesser General Public License
(C) December 2009, BalusC