Minggu, 02 Februari 2014

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 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 instead of having just Map and Map 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, 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 .
*/


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 HttpServletRequest#getParameterXXX() methods to
* ease the processing in @MultipartConfig servlets. You can access the normal request
* parameters by {@link #getParameter(String)} and you can access multiple request
* parameter values by {@link #getParameterValues(String)}.
*


* 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 {@link File}.
*


* 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 {@link #get(Object)}, 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.
*
* @author BalusC
* @link http://balusc.blogspot.com/2009/12/uploading-files-in-servlet-30.html
*/
public class MultipartMap extends HashMap {

// 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 @MultipartConfig
* of the servlet. When the encoding is not specified in the given request, then it will default
* to UTF-8.
* @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 UTF-8.
* @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 getParameterNames() {
return Collections.enumeration(keySet());
}

/**
* @see ServletRequest#getParameterMap()
*/

public Map getParameterMap() {
Map map = new HashMap();
for (Entry 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" %>


lang="en">

</span>Servlet 3.0 file upload test<span class="codetag">



action="upload" method="post" enctype="multipart/form-data">
for="text">Text:
type="text" id="text" name="text" value="${parts.text}">


for="file">File:
type="file" id="file" name="file">
test="${not empty parts.file}">
File ${parts.file.name} successfully uploaded!



for="check1">Check 1:
type="checkbox" id="check1" name="check" value="check1"
${fn:contains(parts.check, 'check1') ? 'checked' : ''}>


for="check2">Check 2:
type="checkbox" id="check2" name="check" value="check2"
${fn:contains(parts.check, 'check2') ? 'checked' : ''}>


type="submit" value="submit">




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 .
*/


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 multipart/form-data and multipart/mixed POST requests and
* will then replace the HttpServletRequest by a {@link MultipartRequest}.
*
* @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 .
*/


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 {@link Part}
* away, but it also provides direct access to the {@link MultipartMap}, 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 getParameterNames() {
return multipartMap.getParameterNames();
}

@Override
public Map 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




Source:http://balusc.blogspot.com/2009/12/uploading-files-in-servlet-30.html

Tidak ada komentar:

Posting Komentar