// Copyright (C) 1998-2001 by Jason Hunter <jhunter_AT_acm_DOT_org>. // All rights reserved. Use of this class is limited. // Please see the LICENSE for more information. package com.oreilly.servlet; import java.io.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; import java.util.Enumeration; import java.util.Map; import java.security.Principal; import com.oreilly.servlet.multipart.MultipartParser; import com.oreilly.servlet.multipart.Part; import com.oreilly.servlet.multipart.FilePart; import com.oreilly.servlet.multipart.ParamPart; import com.oreilly.servlet.multipart.FileRenamePolicy; /** * A utility class to handle <code>multipart/form-data</code> requests, * the kind of requests that support file uploads. This class emulates the * interface of <code>HttpServletRequest</code>, making it familiar to use. * It uses a "push" model where any incoming files are read and saved directly * to disk in the constructor. If you wish to have more flexibility, e.g. * write the files to a database, use the "pull" model * <code>MultipartParser</code> instead. * <p> * This class can receive arbitrarily large files (up to an artificial limit * you can set), and fairly efficiently too. * It cannot handle nested data (multipart content within multipart content). * It <b>can</b> now with the latest release handle internationalized content * (such as non Latin-1 filenames). * <p> * To avoid collisions and have fine control over file placement, there's a * constructor variety that takes a pluggable FileRenamePolicy implementation. * A particular policy can choose to rename or change the location of the file * before it's written. * <p> * See the included upload.war for an example of how to use this class. * <p> * The full file upload specification is contained in experimental RFC 1867, * available at <a href="http://www.ietf.org/rfc/rfc1867.txt"> * http://www.ietf.org/rfc/rfc1867.txt</a>. * * @see MultipartParser * * @author Jason Hunter * @author Geoff Soutter * @version 1.11, 2002/11/01, combine query string params in param list<br> * @version 1.10, 2002/05/27, added access to the original file names<br> * @version 1.9, 2002/04/30, added support for file renaming, thanks to * Changshin Lee<br> * @version 1.8, 2002/04/30, added support for internationalization, thanks to * Changshin Lee<br> * @version 1.7, 2001/02/07, made fields protected to increase user flexibility<br> * @version 1.6, 2000/07/21, redid internals to use MultipartParser, * thanks to Geoff Soutter<br> * @version 1.5, 2000/02/04, added auto MacBinary decoding for IE on Mac<br> * @version 1.4, 2000/01/05, added getParameterValues(), * WebSphere 2.x getContentType() workaround, * stopped writing empty "unknown" file<br> * @version 1.3, 1999/12/28, IE4 on Win98 lastIndexOf("boundary=") * workaround<br> * @version 1.2, 1999/12/20, IE4 on Mac readNextPart() workaround<br> * @version 1.1, 1999/01/15, JSDK readLine() bug workaround<br> * @version 1.0, 1998/09/18<br> */ public class MultipartRequest implements HttpServletRequest { private static final int DEFAULT_MAX_POST_SIZE = 1024 * 1024; // 1 Meg protected Hashtable parameters = new Hashtable(); // name - Vector of values protected Hashtable files = new Hashtable(); // name - UploadedFile protected Cookie[] cookies; protected Locale requestlocale; protected String sessionid; protected String encoding; protected String servletpath; protected String requesturi; protected String remoteuser; protected String querystr; protected String ctxpath; protected String trnpath; protected String pathinfo; protected String formmethod; protected String contenttype; protected int contentlength; protected StringBuffer requesturl; protected String savedir; protected boolean bRequestedSessionIdFromURL; protected boolean bRequestedSessionIdFromCookie; protected boolean bRequestedSessionIdValid; /** * Constructs a new MultipartRequest to handle the specified request, * saving any uploaded files to the given directory, and limiting the * upload size to 1 Megabyte. If the content is too large, an * IOException is thrown. This constructor actually parses the * <tt>multipart/form-data</tt> and throws an IOException if there's any * problem reading or parsing the request. * * @param request the servlet request. * @param saveDirectory the directory in which to save any uploaded files. * @exception IOException if the uploaded content is larger than 1 Megabyte * or there's a problem reading or parsing the request. */ public MultipartRequest(HttpServletRequest request, String saveDirectory) throws IOException { this(request, saveDirectory, DEFAULT_MAX_POST_SIZE); } /** * Constructs a new MultipartRequest to handle the specified request, * saving any uploaded files to the given directory, and limiting the * upload size to the specified length. If the content is too large, an * IOException is thrown. This constructor actually parses the * <tt>multipart/form-data</tt> and throws an IOException if there's any * problem reading or parsing the request. * * @param request the servlet request. * @param saveDirectory the directory in which to save any uploaded files. * @param maxPostSize the maximum size of the POST content. * @exception IOException if the uploaded content is larger than * <tt>maxPostSize</tt> or there's a problem reading or parsing the request. */ public MultipartRequest(HttpServletRequest request, String saveDirectory, int maxPostSize) throws IOException { this(request, saveDirectory, maxPostSize, null, null); } /** * Constructs a new MultipartRequest to handle the specified request, * saving any uploaded files to the given directory, and limiting the * upload size to the specified length. If the content is too large, an * IOException is thrown. This constructor actually parses the * <tt>multipart/form-data</tt> and throws an IOException if there's any * problem reading or parsing the request. * * @param request the servlet request. * @param saveDirectory the directory in which to save any uploaded files. * @param encoding the encoding of the response, such as ISO-8859-1 * @exception IOException if the uploaded content is larger than * 1 Megabyte or there's a problem reading or parsing the request. */ public MultipartRequest(HttpServletRequest request, String saveDirectory, String encoding) throws IOException { this(request, saveDirectory, DEFAULT_MAX_POST_SIZE, encoding, null); } /** * Constructs a new MultipartRequest to handle the specified request, * saving any uploaded files to the given directory, and limiting the * upload size to the specified length. If the content is too large, an * IOException is thrown. This constructor actually parses the * <tt>multipart/form-data</tt> and throws an IOException if there's any * problem reading or parsing the request. * * @param request the servlet request. * @param saveDirectory the directory in which to save any uploaded files. * @param maxPostSize the maximum size of the POST content. * @param encoding the encoding of the response, such as ISO-8859-1 * @exception IOException if the uploaded content is larger than * <tt>maxPostSize</tt> or there's a problem reading or parsing the request. */ public MultipartRequest(HttpServletRequest request, String saveDirectory, int maxPostSize, FileRenamePolicy policy) throws IOException { this(request, saveDirectory, maxPostSize, null, policy); } /** * Constructs a new MultipartRequest to handle the specified request, * saving any uploaded files to the given directory, and limiting the * upload size to the specified length. If the content is too large, an * IOException is thrown. This constructor actually parses the * <tt>multipart/form-data</tt> and throws an IOException if there's any * problem reading or parsing the request. * * @param request the servlet request. * @param saveDirectory the directory in which to save any uploaded files. * @param maxPostSize the maximum size of the POST content. * @param encoding the encoding of the response, such as ISO-8859-1 * @exception IOException if the uploaded content is larger than * <tt>maxPostSize</tt> or there's a problem reading or parsing the request. */ public MultipartRequest(HttpServletRequest request, String saveDirectory, int maxPostSize, String encoding) throws IOException { this(request, saveDirectory, maxPostSize, encoding, null); } /** * Constructs a new MultipartRequest to handle the specified request, * saving any uploaded files to the given directory, and limiting the * upload size to the specified length. If the content is too large, an * IOException is thrown. This constructor actually parses the * <tt>multipart/form-data</tt> and throws an IOException if there's any * problem reading or parsing the request. * * To avoid file collisions, this constructor takes an implementation of the * FileRenamePolicy interface to allow a pluggable rename policy. * * @param request the servlet request. * @param saveDirectory the directory in which to save any uploaded files. * @param maxPostSize the maximum size of the POST content. * @param encoding the encoding of the response, such as ISO-8859-1 * @param policy a pluggable file rename policy * @exception IOException if the uploaded content is larger than * <tt>maxPostSize</tt> or there's a problem reading or parsing the request. */ public MultipartRequest(HttpServletRequest request, String saveDirectory, int maxPostSize, String encoding, FileRenamePolicy policy) throws IOException { // Sanity check values if (request == null) throw new IllegalArgumentException("request cannot be null"); if (saveDirectory == null) throw new IllegalArgumentException("saveDirectory cannot be null"); if (maxPostSize <= 0) { throw new IllegalArgumentException("maxPostSize must be positive"); } requestlocale = request.getLocale(); encoding = request.getCharacterEncoding(); sessionid = request.getRequestedSessionId(); requesturl = request.getRequestURL(); requesturi = request.getRequestURI(); remoteuser = request.getRemoteUser(); querystr = request.getQueryString(); ctxpath = request.getContextPath(); trnpath = request.getPathTranslated(); pathinfo = request.getPathInfo(); servletpath = request.getServletPath(); formmethod = request.getMethod(); cookies = request.getCookies(); // Save the dir File dir = new File(saveDirectory); // Check saveDirectory is truly a directory if (!dir.isDirectory()) throw new IllegalArgumentException("Not a directory: " + saveDirectory); // Check saveDirectory is writable if (!dir.canWrite()) throw new IllegalArgumentException("Not writable: " + saveDirectory); savedir = saveDirectory; // Parse the incoming multipart, storing files in the dir provided, // and populate the meta objects which describe what we found MultipartParser parser = new MultipartParser(request, maxPostSize, true, true, encoding); // Some people like to fetch query string parameters from // MultipartRequest, so here we make that possible. Thanks to // Ben Johnson, ben.johnson@merrillcorp.com, for the idea. if (request.getQueryString() != null) { // Let HttpUtils create a name->String[] structure Hashtable queryParameters = HttpUtils.parseQueryString(request.getQueryString()); // For our own use, name it a name->Vector structure Enumeration queryParameterNames = queryParameters.keys(); while (queryParameterNames.hasMoreElements()) { Object paramName = queryParameterNames.nextElement(); String[] values = (String[])queryParameters.get(paramName); Vector newValues = new Vector(); for (int i = 0; i < values.length; i++) { newValues.add(values[i]); } parameters.put(paramName, newValues); } } Part part; while ((part = parser.readNextPart()) != null) { String name = part.getName(); if (part.isParam()) { // It's a parameter part, add it to the vector of values ParamPart paramPart = (ParamPart) part; String value = paramPart.getStringValue(); Vector existingValues = (Vector)parameters.get(name); if (existingValues == null) { existingValues = new Vector(); parameters.put(name, existingValues); } existingValues.addElement(value); } else if (part.isFile()) { // It's a file part FilePart filePart = (FilePart) part; String fileName = filePart.getFileName(); if (fileName != null) { filePart.setRenamePolicy(policy); // null policy is OK // The part actually contained a file filePart.writeTo(dir); files.put(name, new UploadedFile(dir.toString(), filePart.getFileName(), fileName, filePart.getContentType())); } else { // The field did not contain a file files.put(name, new UploadedFile(null, null, null, null)); } } } bRequestedSessionIdFromURL = request.isRequestedSessionIdFromURL(); bRequestedSessionIdFromCookie = request.isRequestedSessionIdFromCookie(); bRequestedSessionIdValid = request.isRequestedSessionIdValid(); } /** * Constructor with an old signature, kept for backward compatibility. * Without this constructor, a servlet compiled against a previous version * of this class (pre 1.4) would have to be recompiled to link with this * version. This constructor supports the linking via the old signature. * Callers must simply be careful to pass in an HttpServletRequest. * */ public MultipartRequest(ServletRequest request, String saveDirectory) throws IOException { this((HttpServletRequest)request, saveDirectory); } /** * Constructor with an old signature, kept for backward compatibility. * Without this constructor, a servlet compiled against a previous version * of this class (pre 1.4) would have to be recompiled to link with this * version. This constructor supports the linking via the old signature. * Callers must simply be careful to pass in an HttpServletRequest. * */ public MultipartRequest(ServletRequest request, String saveDirectory, int maxPostSize) throws IOException { this((HttpServletRequest)request, saveDirectory, maxPostSize); } public Cookie[] getCookies() { return cookies; } public Locale getLocale() { return requestlocale; } /** * Returns the names of all the parameters as an Enumeration of * Strings. It returns an empty Enumeration if there are no parameters. * * @return the names of all the parameters as an Enumeration of Strings. */ public Enumeration getParameterNames() { return parameters.keys(); } /** * @return number of uploaded files */ public int getFileCount() { int iCount = 0; Enumeration oFiles = files.keys(); Object oFileName; while (oFiles.hasMoreElements()) { oFileName = oFiles.nextElement(); if (null!=oFileName) if (!oFileName.equals("")) iCount++; } // wend return iCount; } // getFileCount /** * Returns the names of all the uploaded files as an Enumeration of * Strings. It returns an empty Enumeration if there are no uploaded * files. Each file name is the name specified by the form, not by * the user. * * @return the names of all the uploaded files as an Enumeration of Strings. */ public Enumeration getFileNames() { return files.keys(); } /** * Returns the value of the named parameter as a String, or null if * the parameter was not sent or was sent without a value. The value * is guaranteed to be in its normal, decoded form. If the parameter * has multiple values, only the last one is returned (for backward * compatibility). For parameters with multiple values, it's possible * the last "value" may be null. * * @param name the parameter name. * @return the parameter value. */ public String getParameter(String name) { try { Vector values = (Vector)parameters.get(name); if (values == null || values.size() == 0) { return null; } String value = (String)values.elementAt(values.size() - 1); return value; } catch (Exception e) { return null; } } /** * Returns the values of the named parameter as a String array, or null if * the parameter was not sent. The array has one entry for each parameter * field sent. If any field was sent without a value that entry is stored * in the array as a null. The values are guaranteed to be in their * normal, decoded form. A single value is returned as a one-element array. * * @param name the parameter name. * @return the parameter values. */ public String[] getParameterValues(String name) { try { Vector values = (Vector)parameters.get(name); if (values == null || values.size() == 0) { return null; } String[] valuesArray = new String[values.size()]; values.copyInto(valuesArray); return valuesArray; } catch (Exception e) { return null; } } /** * Returns the filesystem name of the specified file, or null if the * file was not included in the upload. A filesystem name is the name * specified by the user. It is also the name under which the file is * actually saved. * * @param name the file name. * @return the filesystem name of the file. */ public String getFilesystemName(String name) { try { UploadedFile file = (UploadedFile)files.get(name); return file.getFilesystemName(); // may be null } catch (Exception e) { return null; } } /** * Returns the original filesystem name of the specified file (before any * renaming policy was applied), or null if the file was not included in * the upload. A filesystem name is the name specified by the user. * * @param name the file name. * @return the original file name of the file. */ public String getOriginalFileName(String name) { try { UploadedFile file = (UploadedFile)files.get(name); return file.getOriginalFileName(); // may be null } catch (Exception e) { return null; } } public String getCharacterEncoding() { return encoding; } /** * Returns the content type of the specified file (as supplied by the * client browser), or null if the file was not included in the upload. * * @param name the file name. * @return the content type of the file. */ public String getContentType(String name) { try { UploadedFile file = (UploadedFile)files.get(name); return file.getContentType(); // may be null } catch (Exception e) { return null; } } /** * Returns a File object for the specified file saved on the server's * filesystem, or null if the file was not included in the upload. * @param name the file name. * @return a File object for the named file. */ public File getFile(String name) { try { UploadedFile file = (UploadedFile)files.get(name); return file.getFile(); // may be null } catch (Exception e) { return null; } } /** * Returns a File object for the specified uploaded file * @param number int [0..getFileCount()-1] * @return File */ public File getFile(int number) { UploadedFile file = null; Enumeration fileenum = files.elements(); try { for (int f=0; f<getFileCount(); f++) file = (UploadedFile) fileenum.nextElement(); return file.getFile(); // may be null } catch (Exception e) { return null; } } // getFile public String getContentType() { return contenttype; } public int getContentLength() { return contentlength; } /** * <p>Returns the part of this request's URL that calls the servlet.</p> * Returns the part of this request's URL that calls the servlet. * This includes either the servlet name or a path to the servlet, * but does not include any extra path information or a query string. * Same as the value of the CGI variable SCRIPT_NAME. * @return String containing the name or path of the servlet being called, * as specified in the request URL, decoded. */ public String getServletPath() { return servletpath; } public String getContextPath() { return ctxpath; } public String getPathInfo() { return pathinfo; } public String getPathTranslated() { return trnpath; } public String getMethod() { return formmethod; } public String getRemoteUser() { return remoteuser; } public String getRequestURI() { return requesturi; } public StringBuffer getRequestURL() { return requesturl; } public String getRequestedSessionId() { return sessionid; } public String getQueryString() { return querystr; } public boolean isRequestedSessionIdFromURL() { return bRequestedSessionIdFromURL; } public boolean isRequestedSessionIdFromUrl() { return bRequestedSessionIdFromURL; } public boolean isRequestedSessionIdFromCookie() { return bRequestedSessionIdFromCookie; } public boolean isRequestedSessionIdValid() { return bRequestedSessionIdValid; } public Enumeration getAttributeNames() { throw new RuntimeException("HttpServletRequest.getAttributeNames() method not implemented for MultipartRequest"); } public Object getAttribute(String sAttrName) { throw new RuntimeException("HttpServletRequest.getAttribute() method not implemented for MultipartRequest"); } public void setAttribute(String sAttrName, Object sAttrVal) { throw new RuntimeException("HttpServletRequest.setAttribute() method not implemented for MultipartRequest"); } public void removeAttribute(String sAttrName) { throw new RuntimeException("HttpServletRequest.removeAttribute() method not implemented for MultipartRequest"); } public Enumeration getLocales() { throw new RuntimeException("HttpServletRequest.getLocales() method not implemented for MultipartRequest"); } public boolean isSecure() { throw new RuntimeException("HttpServletRequest.isSecure() method not implemented for MultipartRequest"); } public String getAuthType() { throw new RuntimeException("HttpServletRequest.getAuthType() method not implemented for MultipartRequest"); } public int getLocalPort() { throw new RuntimeException("HttpServletRequest.getLocalPort() method not implemented for MultipartRequest"); } public String getProtocol() { throw new RuntimeException("HttpServletRequest.getProtocol() method not implemented for MultipartRequest"); } public Map getParameterMap() { throw new RuntimeException("HttpServletRequest.getParameterMap() method not implemented for MultipartRequest"); } public String getScheme() { throw new RuntimeException("HttpServletRequest.getScheme() method not implemented for MultipartRequest"); } public String getServerName() { throw new RuntimeException("HttpServletRequest.getServerName() method not implemented for MultipartRequest"); } public int getServerPort() { throw new RuntimeException("HttpServletRequest.getServerPort() method not implemented for MultipartRequest"); } public int getRemotePort() { throw new RuntimeException("HttpServletRequest.getRemotePort() method not implemented for MultipartRequest"); } public String getLocalAddr() { throw new RuntimeException("HttpServletRequest.getLocalAddr() method not implemented for MultipartRequest"); } public String getLocalName() { throw new RuntimeException("HttpServletRequest.getLocalName() method not implemented for MultipartRequest"); } public String getRemoteAddr() { throw new RuntimeException("HttpServletRequest.getRemoteAddr() method not implemented for MultipartRequest"); } public String getRemoteHost() { throw new RuntimeException("HttpServletRequest.getRemoteHost() method not implemented for MultipartRequest"); } public HttpSession getSession() { throw new RuntimeException("HttpServletRequest.getSession() method not implemented for MultipartRequest"); } public HttpSession getSession(boolean b) { throw new RuntimeException("HttpServletRequest.getSession() method not implemented for MultipartRequest"); } public Principal getUserPrincipal() { throw new RuntimeException("HttpServletRequest.getUserPrincipal() method not implemented for MultipartRequest"); } public String getRealPath(String s) { throw new RuntimeException("HttpServletRequest.getRealPath() method not implemented for MultipartRequest"); } public RequestDispatcher getRequestDispatcher(String s) { throw new RuntimeException("HttpServletRequest.getRequestDispatcher() method not implemented for MultipartRequest"); } public boolean isUserInRole(String role) { throw new RuntimeException("HttpServletRequest.isUserInRole() method not implemented for MultipartRequest"); } public String getHeader(String hname) { throw new RuntimeException("HttpServletRequest.getHeader() method not implemented for MultipartRequest"); } public Enumeration getHeaders(String hname) { throw new RuntimeException("HttpServletRequest.getHeaders() method not implemented for MultipartRequest"); } public int getIntHeader(String hname) { throw new RuntimeException("HttpServletRequest.getIntHeader() method not implemented for MultipartRequest"); } public long getDateHeader(String hname) { throw new RuntimeException("HttpServletRequest.getDateHeader() method not implemented for MultipartRequest"); } public Enumeration getHeaderNames() { throw new RuntimeException("HttpServletRequest.getHeaderNames() method not implemented for MultipartRequest"); } public BufferedReader getReader() { throw new RuntimeException("HttpServletRequest.getReader() method not implemented for MultipartRequest"); } public ServletInputStream getInputStream() { throw new RuntimeException("HttpServletRequest.getInputStream() method not implemented for MultipartRequest"); } public void setCharacterEncoding(String sEncoding) { throw new RuntimeException("HttpServletRequest.setCharacterEncoding() method not implemented for MultipartRequest"); } public boolean authenticate(HttpServletResponse response) { return true; } public void login(String username, String password) { } public void logout() { } /* public javax.servlet.http.Part getPart(String name) { return null; } public Collection<javax.servlet.http.Part> getParts() { return null; } public AsyncContext getAsyncContext() { return null; } public ServletContext getServletContext() { return null; } public boolean isAsyncStarted() { return false; } public boolean isAsyncSupported() { return false; } public AsyncContext startAsync() { return null; } public AsyncContext startAsync(ServletRequest request, ServletResponse response ) { return null; } public DispatcherType getDispatcherType() { return null; } */ } // A class to hold information about an uploaded file. // class UploadedFile { private String dir; private String filename; private String original; private String type; UploadedFile(String dir, String filename, String original, String type) { this.dir = dir; this.filename = filename; this.original = original; this.type = type; } public String getContentType() { return type; } public String getFilesystemName() { return filename; } public String getOriginalFileName() { return original; } public File getFile() { if (dir == null || filename == null) { return null; } else { return new File(dir + File.separator + filename); } } }