mschachter 01/04/11 15:56:30 Modified: src/share/org/apache/struts/upload DiskMultipartRequestHandler.java MultipartIterator.java src/upload/org/apache/struts/webapp/upload UploadAction.java UploadForm.java web/upload display.jsp upload.jsp Added: src/share/org/apache/struts/upload BufferedMultipartInputStream.java Log: - Added file BufferedMultipartInputStream to handle buffering for ServletInputStream on it's own, and to provide a safer readLine() method - Added support for indexed values in multipart forms An interesting side to this, the time it takes for a file to upload seems to be dependant on the browser being used to upload. For example there is a big difference between using Internet Explorer 5.5 and Opera 3.6, with Opera being about 7-10 times faster. Revision Changes Path 1.8 +221 -208 jakarta-struts/src/share/org/apache/struts/upload/DiskMultipartRequestHandler.java Index: DiskMultipartRequestHandler.java =================================================================== RCS file: /home/cvs/jakarta-struts/src/share/org/apache/struts/upload/DiskMultipartRequestHandler.java,v retrieving revision 1.7 retrieving revision 1.8 diff -u -r1.7 -r1.8 --- DiskMultipartRequestHandler.java 2001/02/14 21:43:05 1.7 +++ DiskMultipartRequestHandler.java 2001/04/11 22:56:19 1.8 @@ -1,209 +1,222 @@ -package org.apache.struts.upload; - -import java.io.File; -import java.io.IOException; -import java.io.FileOutputStream; -import java.io.ByteArrayInputStream; -import java.io.UnsupportedEncodingException; -import java.util.Hashtable; -import java.util.Enumeration; -import javax.servlet.ServletContext; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; - -import org.apache.struts.action.ActionServlet; -import org.apache.struts.action.ActionMapping; - -/** - * This is a MultipartRequestHandler that writes file data directly to - * to temporary files on disk - * - * @author Mike Schachter - */ -public class DiskMultipartRequestHandler implements MultipartRequestHandler { - - /** - * The ActionServlet instance used for this class - */ - protected ActionServlet servlet; - - /** - * The ActionMapping instance used for this class - */ - protected ActionMapping mapping; - - /** - * A Hashtable representing the form files uploaded - */ - protected Hashtable fileElements; - - /** - * A Hashtable representing the form text input names and values - */ - protected Hashtable textElements; - - /** - * A Hashtable representing all elemnents - */ - protected Hashtable allElements; - - /** - * The temporary directory - */ - protected String tempDir; - - public void handleRequest(HttpServletRequest request) throws ServletException { - - retrieveTempDir(); - - MultipartIterator iterator = new MultipartIterator(request, - servlet.getBufferSize(), - getMaxSizeFromServlet(), - tempDir); - MultipartElement element; - - textElements = new Hashtable(); - fileElements = new Hashtable(); - allElements = new Hashtable(); - - try { - while ((element = iterator.getNextElement()) != null) { - if (!element.isFile()) { - textElements.put(element.getName(), element.getValue()); - allElements.put(element.getName(), element.getValue()); - } - else { - - File tempFile = element.getFile(); - if (tempFile.exists()) { - DiskFile theFile = new DiskFile(tempFile.getAbsolutePath()); - theFile.setContentType(element.getContentType()); - theFile.setFileName(element.getFileName()); - theFile.setFileSize((int) tempFile.length()); - fileElements.put(element.getName(), theFile); - allElements.put(element.getName(), theFile); - } - } - } - } - catch (UnsupportedEncodingException uee) { - throw new ServletException("Encoding \"ISO-8859-1\" not supported"); - } - - } - - public Hashtable getAllElements() { - return allElements; - } - - public Hashtable getTextElements() { - return textElements; - } - - public Hashtable getFileElements() { - return fileElements; - } - - /** - * Delete all the files uploaded - */ - public void rollback() { - Enumeration names = fileElements.keys(); - - while (names.hasMoreElements()) { - String name = (String) names.nextElement(); - DiskFile theFile = (DiskFile) fileElements.get(name); - theFile.destroy(); - } - } - - /** - * Calls on {@link #rollback() rollback()} to delete - * temporary files - */ - public void finish() { - rollback(); - } - - public void setServlet(ActionServlet servlet) { - this.servlet = servlet; - } - - public void setMapping(ActionMapping mapping) { - this.mapping = mapping; - } - - public ActionServlet getServlet() { - return servlet; - } - - public ActionMapping getMapping() { - return mapping; - } - - /** - * Gets the maximum post data size in bytes from the string - * representation in ActionServlet - */ - protected long getMaxSizeFromServlet() throws ServletException{ - String stringSize = servlet.getMaxFileSize(); - long size = -1; - int multiplier = 1; - - if (stringSize.endsWith("K")) { - multiplier = 1024; - stringSize = stringSize.substring(0, stringSize.length()-1); - } - if (stringSize.endsWith("M")) { - multiplier = 1024*1024; - stringSize = stringSize.substring(0, stringSize.length()-1); - } - else if (stringSize.endsWith("G")) { - multiplier = 1024*1024*1024; - stringSize = stringSize.substring(0, stringSize.length()-1); - } - - try { - size = Long.parseLong(stringSize); - } - catch (NumberFormatException nfe) { - throw new ServletException("Invalid format for maximum file size: \"" + - servlet.getMaxFileSize() + "\""); - } - - return (size * multiplier); - } - - /** - * Retrieves the temporary directory from either ActionServlet, a context - * property, or a system property, in that order - */ - protected void retrieveTempDir() { - //get a handle to some temporary file and open - //a stream to it - tempDir = servlet.getTempDir(); - if (tempDir == null) { - //attempt to retrieve the servlet container's temporary directory - ServletContext context = servlet.getServletConfig().getServletContext(); - - try { - tempDir = (String) context.getAttribute("javax.servlet.context.tempdir"); - } - catch (ClassCastException cce) { - tempDir = ((File) context.getAttribute("javax.servlet.context.tempdir")).getAbsolutePath(); - } - - - if (tempDir == null) { - //default to system-wide tempdir - tempDir = System.getProperty("java.io.tmpdir"); - - if (servlet.getDebug() > 1) { - servlet.log("DiskMultipartRequestHandler.handleRequest(): " + - "defaulting to java.io.tmpdir directory \"" + - tempDir); - } - } - } - } +package org.apache.struts.upload; + +import java.io.File; +import java.io.IOException; +import java.io.FileOutputStream; +import java.io.ByteArrayInputStream; +import java.io.UnsupportedEncodingException; +import java.util.Hashtable; +import java.util.Enumeration; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; + +import org.apache.struts.action.ActionServlet; +import org.apache.struts.action.ActionMapping; + +/** + * This is a MultipartRequestHandler that writes file data directly to + * to temporary files on disk + * + * @author Mike Schachter + */ +public class DiskMultipartRequestHandler implements MultipartRequestHandler { + + /** + * The ActionServlet instance used for this class + */ + protected ActionServlet servlet; + + /** + * The ActionMapping instance used for this class + */ + protected ActionMapping mapping; + + /** + * A Hashtable representing the form files uploaded + */ + protected Hashtable fileElements; + + /** + * A Hashtable representing the form text input names and values + */ + protected Hashtable textElements; + + /** + * A Hashtable representing all elemnents + */ + protected Hashtable allElements; + + /** + * The temporary directory + */ + protected String tempDir; + + public void handleRequest(HttpServletRequest request) throws ServletException { + + retrieveTempDir(); + + MultipartIterator iterator = new MultipartIterator(request, + servlet.getBufferSize(), + getMaxSizeFromServlet(), + tempDir); + MultipartElement element; + + textElements = new Hashtable(); + fileElements = new Hashtable(); + allElements = new Hashtable(); + + try { + while ((element = iterator.getNextElement()) != null) { + if (!element.isFile()) { + + String[] textValues = (String[]) textElements.get(element.getName()); + if (textValues != null) { + String[] textValues2 = new String[textValues.length + 1]; + System.arraycopy(textValues, 0, textValues2, 0, textValues.length); + textValues2[textValues.length] = element.getValue(); + textValues = textValues2; + } + else { + textValues = new String[1]; + textValues[0] = element.getValue(); + } + + textElements.put(element.getName(), textValues); + allElements.put(element.getName(), textValues); + } + else { + + File tempFile = element.getFile(); + if (tempFile.exists()) { + DiskFile theFile = new DiskFile(tempFile.getAbsolutePath()); + theFile.setContentType(element.getContentType()); + theFile.setFileName(element.getFileName()); + theFile.setFileSize((int) tempFile.length()); + fileElements.put(element.getName(), theFile); + allElements.put(element.getName(), theFile); + } + } + } + } + catch (UnsupportedEncodingException uee) { + throw new ServletException("Encoding \"ISO-8859-1\" not supported"); + } + + } + + public Hashtable getAllElements() { + return allElements; + } + + public Hashtable getTextElements() { + return textElements; + } + + public Hashtable getFileElements() { + return fileElements; + } + + /** + * Delete all the files uploaded + */ + public void rollback() { + Enumeration names = fileElements.keys(); + + while (names.hasMoreElements()) { + String name = (String) names.nextElement(); + DiskFile theFile = (DiskFile) fileElements.get(name); + theFile.destroy(); + } + } + + /** + * Calls on {@link #rollback() rollback()} to delete + * temporary files + */ + public void finish() { + rollback(); + } + + public void setServlet(ActionServlet servlet) { + this.servlet = servlet; + } + + public void setMapping(ActionMapping mapping) { + this.mapping = mapping; + } + + public ActionServlet getServlet() { + return servlet; + } + + public ActionMapping getMapping() { + return mapping; + } + + /** + * Gets the maximum post data size in bytes from the string + * representation in ActionServlet + */ + protected long getMaxSizeFromServlet() throws ServletException{ + String stringSize = servlet.getMaxFileSize(); + long size = -1; + int multiplier = 1; + + if (stringSize.endsWith("K")) { + multiplier = 1024; + stringSize = stringSize.substring(0, stringSize.length()-1); + } + if (stringSize.endsWith("M")) { + multiplier = 1024*1024; + stringSize = stringSize.substring(0, stringSize.length()-1); + } + else if (stringSize.endsWith("G")) { + multiplier = 1024*1024*1024; + stringSize = stringSize.substring(0, stringSize.length()-1); + } + + try { + size = Long.parseLong(stringSize); + } + catch (NumberFormatException nfe) { + throw new ServletException("Invalid format for maximum file size: \"" + + servlet.getMaxFileSize() + "\""); + } + + return (size * multiplier); + } + + /** + * Retrieves the temporary directory from either ActionServlet, a context + * property, or a system property, in that order + */ + protected void retrieveTempDir() { + //get a handle to some temporary file and open + //a stream to it + tempDir = servlet.getTempDir(); + if (tempDir == null) { + //attempt to retrieve the servlet container's temporary directory + ServletContext context = servlet.getServletConfig().getServletContext(); + + try { + tempDir = (String) context.getAttribute("javax.servlet.context.tempdir"); + } + catch (ClassCastException cce) { + tempDir = ((File) context.getAttribute("javax.servlet.context.tempdir")).getAbsolutePath(); + } + + + if (tempDir == null) { + //default to system-wide tempdir + tempDir = System.getProperty("java.io.tmpdir"); + + if (servlet.getDebug() > 1) { + servlet.log("DiskMultipartRequestHandler.handleRequest(): " + + "defaulting to java.io.tmpdir directory \"" + + tempDir); + } + } + } + } } 1.10 +547 -498 jakarta-struts/src/share/org/apache/struts/upload/MultipartIterator.java Index: MultipartIterator.java =================================================================== RCS file: /home/cvs/jakarta-struts/src/share/org/apache/struts/upload/MultipartIterator.java,v retrieving revision 1.9 retrieving revision 1.10 diff -u -r1.9 -r1.10 --- MultipartIterator.java 2001/02/14 21:43:06 1.9 +++ MultipartIterator.java 2001/04/11 22:56:20 1.10 @@ -1,498 +1,547 @@ -package org.apache.struts.upload; - -import java.io.File; -import java.io.InputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.io.FileOutputStream; -import java.io.UnsupportedEncodingException; -import javax.servlet.ServletException; -import javax.servlet.ServletInputStream; -import javax.servlet.http.HttpServletRequest; - -/** - * The MultipartIterator class is responsible for reading the - * input data of a multipart request and splitting it up into - * input elements, wrapped inside of a - * {@link org.apache.struts.upload.MultipartElement MultipartElement} - * for easy definition. To use this class, create a new instance - * of MultipartIterator passing it a HttpServletRequest in the - * constructor. Then use the {@link #getNextElement() getNextElement} - * method until it returns null, then you're finished. Example: <br> - * <pre> - * MultipartIterator iterator = new MultipartIterator(request); - * MultipartElement element; - * - * while ((element = iterator.getNextElement()) != null) { - * //do something with element - * } - * </pre> - * - * @see org.apache.struts.upload.MultipartElement - * @author Mike Schachter - */ -public class MultipartIterator { - - /** - * The request instance for this class - */ - protected HttpServletRequest request; - - /** - * The input stream instance for this class - */ - protected ServletInputStream inputStream; - - /** - * The boundary for this multipart request - */ - protected String boundary; - - /** - * Whether or not the input stream is finished - */ - protected boolean contentRead = false; - - /** - * The maximum file size in bytes allowed. Ignored if -1 - */ - protected long maxSize = -1; - - /** - * The total bytes read from this request - */ - protected long totalLength = 0; - - /** - * The content length of this request - */ - protected int contentLength; - - /** - * The amount of data read from a request at a time. - * This also represents the maximum size in bytes of - * a line read from the request - * Defaults to 4 * 1024 (4 KB) - */ - protected int bufferSize = 4 * 1024; - - /** - * The temporary directory to store files - */ - protected String tempDir; - - /** - * Constructs a MultipartIterator with a default buffer size and no file size - * limit - * - * @param request The multipart request to iterate - */ - public MultipartIterator(HttpServletRequest request) throws ServletException{ - this(request, -1); - } - - /** - * Constructs a MultipartIterator with the specified buffer size and - * no file size limit - * - * @param request The multipart request to iterate - * @param bufferSize The size in bytes that should be read from the input - * stream at a times - */ - public MultipartIterator(HttpServletRequest request, int bufferSize) throws ServletException { - this (request, bufferSize, -1); - } - - /** - * Constructs a MultipartIterator with the specified buffer size and - * the specified file size limit in bytes - * - * @param request The multipart request to iterate - * @param bufferSize The size in bytes that should be read from the input - * stream at a times - * @param maxSize The maximum size in bytes allowed for a multipart element's data - */ - public MultipartIterator(HttpServletRequest request, int bufferSize, long maxSize) - throws ServletException { - - this(request, bufferSize, maxSize, null); - - } - - public MultipartIterator(HttpServletRequest request, - int bufferSize, - long maxSize, - String tempDir) throws ServletException { - - this.request = request; - this.maxSize = maxSize; - if (bufferSize > -1) { - this.bufferSize = bufferSize; - } - if (tempDir != null) { - this.tempDir = tempDir; - } - else { - //default to system-wide tempdir - tempDir = System.getProperty("java.io.tmpdir"); - } - parseRequest(); - } - - /** - * Retrieves the next element in the iterator if one exists. - * - * @throws a ServletException if the post size exceeds the maximum file size - * passed in the 3 argument constructor - * @throws an UnsupportedEncodingException if the "ISO-8859-1" encoding isn't found - * @return a {@link org.apache.struts.upload.MultipartElement MultipartElement} - * representing the next element in the request data - * - */ - public MultipartElement getNextElement() throws ServletException, UnsupportedEncodingException { - //retrieve the "Content-Disposition" header - //and parse - String disposition = readLine(); - - if ((disposition != null) && (disposition.startsWith("Content-Disposition"))) { - String name = parseDispositionName(disposition); - String filename = parseDispositionFilename(disposition); - - String contentType = null; - boolean isFile = (filename != null); - - if (isFile) { - filename = new File(filename).getName(); - - //check for windows filenames, - //from linux jdk's the entire filepath - //isn't parsed correctly from File.getName() - int colonIndex = filename.indexOf(":"); - int slashIndex = filename.lastIndexOf("\\"); - - if ((colonIndex > -1) && (slashIndex > -1)) { - //then consider this filename to be a full - //windows filepath, and parse it accordingly - //to retrieve just the file name - filename = filename.substring(slashIndex+1, filename.length()); - } - - - - //get the content type - contentType = readLine(); - contentType = parseContentType(contentType); - } - - - - //ignore next line (whitespace) (unless it's a file - //without content-type) - if (! ((isFile) && contentType == null)) { - readLine(); - } - - MultipartElement element = null; - - //process a file element - if (isFile) { - try { - //create a local file on disk representing the element - File elementFile = createLocalFile(); - - element = new MultipartElement(name, filename, contentType, elementFile); - } catch (IOException ioe) { - throw new ServletException("IOException while reading file element: ioe.getMessage()", ioe); - } - } - else { - //read data into String form, then convert to bytes - //for text - StringBuffer textData = new StringBuffer(); - String line; - //parse for text data - line = readLine(); - - while ((line != null) && (!line.startsWith(boundary))) { - textData.append(line); - line = readLine(); - - if (maxSize > -1) { - if (totalLength > maxSize) { - throw new ServletException("Multipart data size exceeds the maximum " + - "allowed post size"); - } - } - } - - if (textData.length() > 1) { - //cut off "\r\n" from the end - textData.setLength(textData.length()-2); - } - - //create the element - element = new MultipartElement(name, textData.toString()); - } - return element; - } - //reset stream - if (inputStream.markSupported()) { - try { - inputStream.reset(); - } - catch (IOException ioe) { - throw new ServletException("IOException while resetting input stream: " + - ioe.getMessage()); - } - } - return null; - } - - /** - * Set the maximum amount of bytes read from a line at one time - * - * @see javax.servlet.ServletInputStream#readLine(byte[], int, int) - */ - public void setBufferSize(int bufferSize) { - this.bufferSize = bufferSize; - } - - /** - * Get the maximum amount of bytes read from a line at one time - * - * @see javax.servlet.ServletInputStream#readLine(byte[], int, int) - */ - public int getBufferSize() { - return bufferSize; - } - - /** - * Set the maximum post data size allowed for a multipart request - * @param maxSize The maximum post data size in bytes, set to <code>-1</code> - * for no limit - */ - public void setMaxSize(long maxSize) { - this.maxSize = maxSize; - } - - /** - * Get the maximum post data size allowed for a multipart request - * @return The maximum post data size in bytes - */ - public long getMaxSize() { - return maxSize; - } - - /** - * Handles retrieving the boundary and setting the input stream - */ - protected void parseRequest() throws ServletException { - - contentLength = request.getContentLength(); - - //set boundary - boundary = parseBoundary(request.getContentType()); - - try { - //set the input stream - inputStream = request.getInputStream(); - //mark the input stream to allow multiple reads - if (inputStream.markSupported()) { - inputStream.mark(contentLength+1); - } - - } - catch (IOException ioe) { - throw new ServletException("MultipartIterator.parseRequest(): " + - "IOException while trying to obtain " + - "ServletInputStream"); - } - - if ((boundary == null) || (boundary.length() < 1)) { - //try retrieving the header through more "normal" means - boundary = parseBoundary(request.getHeader("Content-type")); - } - - if ((boundary == null) || (boundary.length() < 1)) { - throw new ServletException("MultipartIterator: cannot retrieve boundary " + - "for multipart request"); - } - - //read first line - try { - String firstLine = readLine(); - - if (firstLine == null) { - throw new ServletException("MultipartIterator: no multipart request data " + - "sent"); - } - if (!firstLine.startsWith(boundary)) { - throw new ServletException("MultipartIterator: invalid multipart request " + - "data, doesn't start with boundary"); - } - } - catch (UnsupportedEncodingException uee) { - throw new ServletException("MultipartIterator: encoding \"ISO-8859-1\" not supported"); - } - } - - /** - * Parses a content-type String for the boundary. Appends a - * "--" to the beginning of the boundary, because thats the - * real boundary as opposed to the shortened one in the - * content type. - */ - public static String parseBoundary(String contentType) { - if (contentType.lastIndexOf("boundary=") != -1) { - String _boundary = "--" + - contentType.substring(contentType.lastIndexOf("boundary=")+9); - if (_boundary.endsWith("\n")) { - //strip it off - return _boundary.substring(0, _boundary.length()-1); - } - return _boundary; - } - return null; - } - - /** - * Parses the "Content-Type" line of a multipart form for a content type - * - * @param contentTypeString A String reprsenting the Content-Type line, - * with a trailing "\n" - * @return The content type specified, or <code>null</code> if one can't be - * found. - */ - public static String parseContentType(String contentTypeString) { - int nameIndex = contentTypeString.indexOf("Content-Type: "); - - if (nameIndex != -1) { - int endLineIndex = contentTypeString.indexOf("\n"); - if (endLineIndex != -1) { - return contentTypeString.substring(nameIndex+14, endLineIndex); - } - } - return null; - } - - /** - * Retrieves the "name" attribute from a content disposition line - * - * @param dispositionString The entire "Content-disposition" string - * @return <code>null</code> if no name could be found, otherwise, - * returns the name - * @see #parseForAttribute(String, String) - */ - public static String parseDispositionName(String dispositionString) { - return parseForAttribute("name", dispositionString); - } - - /** - * Retrieves the "filename" attribute from a content disposition line - * - * @param dispositionString The entire "Content-disposition" string - * @return <code>null</code> if no filename could be found, otherwise, - * returns the filename - * @see #parseForAttribute(String, String) - */ - public static String parseDispositionFilename(String dispositionString) { - return parseForAttribute("filename", dispositionString); - } - - - /** - * Parses a string looking for a attribute-value pair, and returns the value. - * For example: - * <pre> - * String parseString = "Content-Disposition: filename=\"bob\" name=\"jack\""; - * MultipartIterator.parseForAttribute(parseString, "name"); - * </pre> - * That will return "bob". - * - * @param attribute The name of the attribute you're trying to get - * @param parseString The string to retrieve the value from - * @return The value of the attribute, or <code>null</code> if none could be found - */ - public static String parseForAttribute(String attribute, String parseString) { - int nameIndex = parseString.indexOf(attribute + "=\""); - if (nameIndex != -1) { - - int endQuoteIndex = parseString.indexOf("\"", nameIndex+attribute.length()+3); - - if (endQuoteIndex != -1) { - return parseString.substring(nameIndex+attribute.length()+2, endQuoteIndex); - } - return ""; - } - return null; - } - - /** - * Reads the input stream until it reaches a new line - */ - protected String readLine() throws ServletException, UnsupportedEncodingException { - - byte[] bufferByte = new byte[bufferSize]; - int bytesRead; - - if (totalLength >= contentLength) { - return null; - } - - try { - bytesRead = inputStream.readLine(bufferByte, - 0, - bufferSize); - } - catch (IOException ioe) { - throw new ServletException("IOException while reading multipart request: " + - ioe.getMessage()); - } - if (bytesRead == -1) { - return null; - } - - totalLength += bytesRead; - return new String(bufferByte, 0, bytesRead, "ISO-8859-1"); - } - - /** - * Creates a file on disk from the current mulitpart element - * @param fileName the name of the multipart file - */ - protected File createLocalFile() throws IOException,ServletException { - - File tempFile = File.createTempFile("strts", null, new File(tempDir)); - OutputStream fos = new FileOutputStream(tempFile); - - byte[] bufferBytes = new byte[bufferSize]; - InputStream requestIn = new MultipartValueStream(inputStream, boundary); - - int bytesRead = 0; - while (bytesRead != -1) { - - bytesRead = requestIn.read(bufferBytes); - - if (bytesRead > 0) { - totalLength += bytesRead; - } - - //check to make sure the read doesn't exceed the bounds - if (bytesRead > 0) { - if (maxSize > -1) { - if (totalLength > maxSize) { - throw new ServletException("Multipart data size exceeds the maximum " + - "allowed post size"); - } - } - } - if (bytesRead > 0) { - fos.write(bufferBytes, 0, bytesRead); - } - } - - fos.close(); - return tempFile; - } - -} +package org.apache.struts.upload; + +import java.io.File; +import java.io.IOException; +import java.io.BufferedOutputStream; +import java.io.FileOutputStream; +import java.io.UnsupportedEncodingException; +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; + +/** + * The MultipartIterator class is responsible for reading the + * input data of a multipart request and splitting it up into + * input elements, wrapped inside of a + * {@link org.apache.struts.upload.MultipartElement MultipartElement} + * for easy definition. To use this class, create a new instance + * of MultipartIterator passing it a HttpServletRequest in the + * constructor. Then use the {@link #getNextElement() getNextElement} + * method until it returns null, then you're finished. Example: <br> + * <pre> + * MultipartIterator iterator = new MultipartIterator(request); + * MultipartElement element; + * + * while ((element = iterator.getNextElement()) != null) { + * //do something with element + * } + * </pre> + * + * @see org.apache.struts.upload.MultipartElement + * @author Mike Schachter + */ +public class MultipartIterator { + + /** + * The maximum size in bytes of the buffer used to read lines [4K] + */ + public static int MAX_LINE_SIZE = 4096; + + /** + * The request instance for this class + */ + protected HttpServletRequest request; + + /** + * The input stream instance for this class + */ + protected BufferedMultipartInputStream inputStream; + + /** + * The boundary for this multipart request + */ + protected String boundary; + + /** + * The byte array representing the boundary for this multipart request + */ + protected byte[] boundaryBytes; + + /** + * Whether or not the input stream is finished + */ + protected boolean contentRead = false; + + /** + * The maximum file size in bytes allowed. Ignored if -1 + */ + protected long maxSize = -1; + + /** + * The total bytes read from this request + */ + protected long totalLength = 0; + + /** + * The content length of this request + */ + protected int contentLength; + + /** + * The size in bytes written to the filesystem at a time [20K] + */ + protected int diskBufferSize = 2 * 10240; + + /** + * The amount of data read from a request at a time. + * This also represents the maximum size in bytes of + * a line read from the request [4KB] + */ + protected int bufferSize = 4096; + + /** + * The temporary directory to store files + */ + protected String tempDir; + + /** + * Constructs a MultipartIterator with a default buffer size and no file size + * limit + * + * @param request The multipart request to iterate + */ + public MultipartIterator(HttpServletRequest request) throws ServletException{ + this(request, -1); + } + + /** + * Constructs a MultipartIterator with the specified buffer size and + * no file size limit + * + * @param request The multipart request to iterate + * @param bufferSize The size in bytes that should be read from the input + * stream at a times + */ + public MultipartIterator(HttpServletRequest request, int bufferSize) throws ServletException { + this (request, bufferSize, -1); + } + + /** + * Constructs a MultipartIterator with the specified buffer size and + * the specified file size limit in bytes + * + * @param request The multipart request to iterate + * @param bufferSize The size in bytes that should be read from the input + * stream at a times + * @param maxSize The maximum size in bytes allowed for a multipart element's data + */ + public MultipartIterator(HttpServletRequest request, int bufferSize, long maxSize) + throws ServletException { + + this(request, bufferSize, maxSize, null); + + } + + public MultipartIterator(HttpServletRequest request, + int bufferSize, + long maxSize, + String tempDir) throws ServletException { + + this.request = request; + this.maxSize = maxSize; + if (bufferSize > -1) { + this.bufferSize = bufferSize; + } + if (tempDir != null) { + this.tempDir = tempDir; + } + else { + //default to system-wide tempdir + tempDir = System.getProperty("java.io.tmpdir"); + } + parseRequest(); + } + + /** + * Retrieves the next element in the iterator if one exists. + * + * @throws a ServletException if the post size exceeds the maximum file size + * passed in the 3 argument constructor + * @throws an UnsupportedEncodingException if the "ISO-8859-1" encoding isn't found + * @return a {@link org.apache.struts.upload.MultipartElement MultipartElement} + * representing the next element in the request data + * + */ + public MultipartElement getNextElement() throws ServletException, UnsupportedEncodingException { + //retrieve the "Content-Disposition" header + //and parse + String disposition = readLine(); + + + if ((disposition != null) && (disposition.startsWith("Content-Disposition"))) { + String name = parseDispositionName(disposition); + String filename = parseDispositionFilename(disposition); + + String contentType = null; + boolean isFile = (filename != null); + + if (isFile) { + filename = new File(filename).getName(); + + //check for windows filenames, + //from linux jdk's the entire filepath + //isn't parsed correctly from File.getName() + int colonIndex = filename.indexOf(":"); + int slashIndex = filename.lastIndexOf("\\"); + + if ((colonIndex > -1) && (slashIndex > -1)) { + //then consider this filename to be a full + //windows filepath, and parse it accordingly + //to retrieve just the file name + filename = filename.substring(slashIndex+1, filename.length()); + } + + //get the content type + contentType = readLine(); + contentType = parseContentType(contentType); + } + + + + //ignore next line (whitespace) (unless it's a file + //without content-type) + if (! ((isFile) && contentType == null)) { + readLine(); + } + + MultipartElement element = null; + + //process a file element + if (isFile) { + try { + //create a local file on disk representing the element + File elementFile = createLocalFile(); + + element = new MultipartElement(name, filename, contentType, elementFile); + } catch (IOException ioe) { + throw new ServletException("IOException while reading file element: ioe.getMessage()", ioe); + } + } + else { + //read data into String form, then convert to bytes + //for text + StringBuffer textData = new StringBuffer(); + String line; + //parse for text data + line = readLine(); + + while ((line != null) && (!line.startsWith(boundary))) { + textData.append(line); + line = readLine(); + + if (maxSize > -1) { + if (totalLength > maxSize) { + throw new ServletException("Multipart data size exceeds the maximum " + + "allowed post size"); + } + } + } + + if (textData.length() > 0) { + //cut off "\r" from the end if necessary + if (textData.charAt(textData.length()-1) == '\r') { + textData.setLength(textData.length()-1); + } + } + + //create the element + element = new MultipartElement(name, textData.toString()); + } + return element; + } + + //reset stream + if (inputStream.markSupported()) { + try { + inputStream.reset(); + } + catch (IOException ioe) { + throw new ServletException("IOException while resetting input stream: " + + ioe.getMessage()); + } + } + return null; + } + + /** + * Set the maximum amount of bytes read from a line at one time + * + * @see javax.servlet.ServletInputStream#readLine(byte[], int, int) + */ + public void setBufferSize(int bufferSize) { + this.bufferSize = bufferSize; + } + + /** + * Get the maximum amount of bytes read from a line at one time + * + * @see javax.servlet.ServletInputStream#readLine(byte[], int, int) + */ + public int getBufferSize() { + return bufferSize; + } + + /** + * Set the maximum post data size allowed for a multipart request + * @param maxSize The maximum post data size in bytes, set to <code>-1</code> + * for no limit + */ + public void setMaxSize(long maxSize) { + this.maxSize = maxSize; + } + + /** + * Get the maximum post data size allowed for a multipart request + * @return The maximum post data size in bytes + */ + public long getMaxSize() { + return maxSize; + } + + /** + * Handles retrieving the boundary and setting the input stream + */ + protected void parseRequest() throws ServletException { + + contentLength = request.getContentLength(); + + //set boundary + boundary = parseBoundary(request.getContentType()); + boundaryBytes = boundary.getBytes(); + + try { + //set the input stream + inputStream = new BufferedMultipartInputStream(request.getInputStream(), + bufferSize, + contentLength, + maxSize); + //mark the input stream to allow multiple reads + if (inputStream.markSupported()) { + inputStream.mark(contentLength+1); + } + + } + catch (IOException ioe) { + throw new ServletException("MultipartIterator.parseRequest(): " + + "IOException while trying to obtain " + + "ServletInputStream"); + } + + if ((boundary == null) || (boundary.length() < 1)) { + //try retrieving the header through more "normal" means + boundary = parseBoundary(request.getHeader("Content-type")); + } + + if ((boundary == null) || (boundary.length() < 1)) { + throw new ServletException("MultipartIterator: cannot retrieve boundary " + + "for multipart request"); + } + + //read first line + try { + String firstLine = readLine(); + + if (firstLine == null) { + throw new ServletException("MultipartIterator: no multipart request data " + + "sent"); + } + if (!firstLine.startsWith(boundary)) { + throw new ServletException("MultipartIterator: invalid multipart request " + + "data, doesn't start with boundary"); + } + } + catch (UnsupportedEncodingException uee) { + throw new ServletException("MultipartIterator: encoding \"ISO-8859-1\" not supported"); + } + } + + /** + * Parses a content-type String for the boundary. Appends a + * "--" to the beginning of the boundary, because thats the + * real boundary as opposed to the shortened one in the + * content type. + */ + public static String parseBoundary(String contentType) { + if (contentType.lastIndexOf("boundary=") != -1) { + String _boundary = "--" + + contentType.substring(contentType.lastIndexOf("boundary=")+9); + if (_boundary.endsWith("\n")) { + //strip it off + return _boundary.substring(0, _boundary.length()-1); + } + return _boundary; + } + return null; + } + + /** + * Parses the "Content-Type" line of a multipart form for a content type + * + * @param contentTypeString A String reprsenting the Content-Type line, + * with a trailing "\n" + * @return The content type specified, or <code>null</code> if one can't be + * found. + */ + public static String parseContentType(String contentTypeString) { + int nameIndex = contentTypeString.indexOf("Content-Type: "); + + if (nameIndex != -1) { + int endLineIndex = contentTypeString.indexOf("\n"); + if (endLineIndex == -1) { + endLineIndex = contentTypeString.length()-1; + } + return contentTypeString.substring(nameIndex+14, endLineIndex); + } + return null; + } + + /** + * Retrieves the "name" attribute from a content disposition line + * + * @param dispositionString The entire "Content-disposition" string + * @return <code>null</code> if no name could be found, otherwise, + * returns the name + * @see #parseForAttribute(String, String) + */ + public static String parseDispositionName(String dispositionString) { + return parseForAttribute("name", dispositionString); + } + + /** + * Retrieves the "filename" attribute from a content disposition line + * + * @param dispositionString The entire "Content-disposition" string + * @return <code>null</code> if no filename could be found, otherwise, + * returns the filename + * @see #parseForAttribute(String, String) + */ + public static String parseDispositionFilename(String dispositionString) { + return parseForAttribute("filename", dispositionString); + } + + + /** + * Parses a string looking for a attribute-value pair, and returns the value. + * For example: + * <pre> + * String parseString = "Content-Disposition: filename=\"bob\" name=\"jack\""; + * MultipartIterator.parseForAttribute(parseString, "name"); + * </pre> + * That will return "bob". + * + * @param attribute The name of the attribute you're trying to get + * @param parseString The string to retrieve the value from + * @return The value of the attribute, or <code>null</code> if none could be found + */ + public static String parseForAttribute(String attribute, String parseString) { + int nameIndex = parseString.indexOf(attribute + "=\""); + if (nameIndex != -1) { + + int endQuoteIndex = parseString.indexOf("\"", nameIndex+attribute.length()+3); + + if (endQuoteIndex != -1) { + return parseString.substring(nameIndex+attribute.length()+2, endQuoteIndex); + } + return ""; + } + return null; + } + + /** + * Reads the input stream until it reaches a new line + */ + protected String readLine() throws ServletException, UnsupportedEncodingException { + + byte[] bufferByte = new byte[bufferSize]; + int bytesRead; + + if (totalLength >= contentLength) { + return null; + } + + try { + bytesRead = inputStream.readLine(bufferByte, + 0, + bufferSize); + } + catch (IOException ioe) { + throw new ServletException("IOException while reading multipart request: " + + ioe.getMessage()); + } + if (bytesRead == -1) { + return null; + } + + totalLength += bytesRead; + return new String(bufferByte, 0, bytesRead, "ISO-8859-1"); + } + + /** + * Creates a file on disk from the current mulitpart element + * @param fileName the name of the multipart file + */ + protected File createLocalFile() throws IOException { + + File tempFile = File.createTempFile("strts", null, new File(tempDir)); + BufferedOutputStream fos = new BufferedOutputStream(new FileOutputStream(tempFile), + diskBufferSize); + byte[] lineBuffer = new byte[MAX_LINE_SIZE]; + int bytesRead = inputStream.readLine(lineBuffer, 0, MAX_LINE_SIZE); + + boolean cutCarriage = false; + boolean cutNewline = false; + + while ((bytesRead != -1) && (!equals(lineBuffer, 0, boundaryBytes.length, + boundaryBytes))) { + + if (cutCarriage) { + fos.write('\r'); + } + if (cutNewline) { + fos.write('\n'); + } + if (bytesRead > 0) { + if (lineBuffer[bytesRead-1] == '\r') { + bytesRead--; + cutCarriage = true; + } + else { + cutCarriage = false; + } + } + cutNewline = true; + fos.write(lineBuffer, 0, bytesRead); + bytesRead = inputStream.readLine(lineBuffer, 0, MAX_LINE_SIZE); + } + + fos.flush(); + fos.close(); + return tempFile; + } + + /** + * Checks bytes for equality. Two byte arrays are equal if + * each of their elements are the same. This method checks + * comp[offset] with source[0] to source[length-1] with + * comp[offset + length - 1] + * @param comp The byte to compare to <code>source</code> + * @param offset The offset to start at in <code>comp</code> + * @param length The length of <code>comp</code> to compare to + * @param source The reference byte to test for equality + */ + public static boolean equals(byte[] comp, int offset, int length, + byte[] source) { + + if (length != source.length) { + return false; + } + + for (int i = 0; i < length; i++) { + if (comp[offset+i] != source[i]) { + return false; + } + } + return true; + } + +} 1.1 jakarta-struts/src/share/org/apache/struts/upload/BufferedMultipartInputStream.java Index: BufferedMultipartInputStream.java =================================================================== package org.apache.struts.upload; import java.io.IOException; import java.io.InputStream; /** * This class implements buffering for an InputStream as well as a * readLine method. The purpose of this is to provide a reliable * readLine() method. */ public class BufferedMultipartInputStream extends InputStream { /** * The underlying InputStream used by this class */ protected InputStream inputStream; /** * The byte array used to hold buffered data */ protected byte[] buffer; /** * The current offset we're at in the buffer's byte array */ protected int bufferOffset = 0; /** * The size of the byte array buffer */ protected int bufferSize = 8192; /** * The number of bytes read from the underlying InputStream that are * in the buffer */ protected int bufferLength = 0; /** * The total number of bytes read so far */ protected int totalLength = 0; /** * The content length of the multipart data */ protected long contentLength; /** * The maximum allowed size for the multipart data, or -1 for an unlimited * maximum file length */ protected long maxSize = -1; /** * Public constructor for this class, just wraps the InputStream * given * @param inputStream The underlying stream to read from * @param bufferSize The size in bytes of the internal buffer * @param contentLength The content length for this request * @param maxSize The maximum size in bytes that this multipart * request can be, or -1 for an unlimited length */ public BufferedMultipartInputStream(InputStream inputStream, int bufferSize, long contentLength, long maxSize) throws IOException { this.inputStream = inputStream; this.bufferSize = bufferSize; this.contentLength = contentLength; this.maxSize = maxSize; buffer = new byte[bufferSize]; fill(); } /** * This method returns the number of available bytes left to read * in the buffer before it has to be refilled */ public int available() { return bufferLength - bufferOffset; } /** * This method attempts to close the underlying InputStream */ public void close() throws IOException { inputStream.close(); } /** * This method calls on the mark() method of the underlying InputStream */ public void mark(int position) { inputStream.mark(position); } /** * This method calls on the markSupported() method of the underlying InputStream * @return Whether or not the underlying InputStream supports marking */ public boolean markSupported() { return inputStream.markSupported(); } /** * This method returns the next byte in the buffer, and refills it if necessary. * @return The next byte read in the buffer, or -1 if the end of the stream has * been reached */ public int read() throws IOException { if (buffer == null) { return -1; } if (bufferOffset < bufferLength) { return (int)(char) buffer[bufferOffset++]; } fill(); return read(); } /** * This method populates the byte array <code>b</code> with data up to * <code>b.length</code> bytes */ public int read(byte[] b) throws IOException { return read(b, 0, b.length); } /** * This method populates the byte array <code>b</code> with data up to * <code>length</code> starting at b[offset] */ public int read(byte[] b, int offset, int length) throws IOException { int count = 0; int read = read(); if (read == -1) { return -1; } while ((read != -1) && (count < length)) { b[offset] = (byte) read; read = read(); count++; offset++; } return count; } /** * This method reads into the byte array <code>b</code> until * a newline ('\n') character is encountered or the number of bytes * specified by <code>length</code> have been read */ public int readLine(byte[] b, int offset, int length) throws IOException { int count = 0; int read = read(); if (read == -1) { return -1; } while ((read != -1) && (count < length)) { if (read == '\n') break; b[offset] = (byte) read; count++; offset++; read = read(); } return count; } /** * This method makes a call to the reset() method of the underlying * InputStream */ public void reset() throws IOException { inputStream.reset(); } /** * Fills the buffer with data from the underlying inputStream. If it can't * fill the entire buffer in one read, it will read as many times as necessary * to fill the buffer */ protected void fill() throws IOException { if ((bufferOffset > -1) && (bufferLength > -1)) { int length = Math.min(bufferSize, (int) contentLength); if (maxSize > -1) { length = Math.min(length, (int) maxSize); } int bytesRead = inputStream.read(buffer, 0, length); if (bytesRead == -1) { buffer = null; bufferOffset = -1; bufferLength = -1; } else { bufferLength = bytesRead; totalLength += bytesRead; bufferOffset = 0; } } } } 1.2 +37 -8 jakarta-struts/src/upload/org/apache/struts/webapp/upload/UploadAction.java Index: UploadAction.java =================================================================== RCS file: /home/cvs/jakarta-struts/src/upload/org/apache/struts/webapp/upload/UploadAction.java,v retrieving revision 1.1 retrieving revision 1.2 diff -u -r1.1 -r1.2 --- UploadAction.java 2001/03/22 13:17:10 1.1 +++ UploadAction.java 2001/04/11 22:56:23 1.2 @@ -1,7 +1,7 @@ /* - * $Header: /home/cvs/jakarta-struts/src/upload/org/apache/struts/webapp/upload/UploadAction.java,v 1.1 2001/03/22 13:17:10 rleland Exp $ - * $Revision: 1.1 $ - * $Date: 2001/03/22 13:17:10 $ + * $Header: /home/cvs/jakarta-struts/src/upload/org/apache/struts/webapp/upload/UploadAction.java,v 1.2 2001/04/11 22:56:23 mschachter Exp $ + * $Revision: 1.2 $ + * $Date: 2001/04/11 22:56:23 $ * * ==================================================================== * @@ -64,6 +64,8 @@ import java.io.InputStream; import java.io.IOException; +import java.io.OutputStream; +import java.io.FileOutputStream; import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; @@ -85,7 +87,7 @@ * page to display them * * @author Mike Schachter - * @version $Revision: 1.1 $ $Date: 2001/03/22 13:17:10 $ + * @version $Revision: 1.2 $ $Date: 2001/04/11 22:56:23 $ */ @@ -111,6 +113,8 @@ //retrieve the content type String contentType = file.getContentType(); + + boolean writeFile = theForm.getWriteFile(); //retrieve the file size String size = (file.getFileSize() + " bytes"); @@ -120,12 +124,37 @@ try { //retrieve the file data ByteArrayOutputStream baos = new ByteArrayOutputStream(); - InputStream stream = file.getInputStream(); - byte[] buffer = new byte[file.getFileSize()]; - stream.read(buffer); - baos.write(buffer); - data = new String(baos.toByteArray()); + if (!writeFile) { + //only write files out that are less than 1MB + if (file.getFileSize() < (4*1024000)) { + + byte[] buffer = new byte[8192]; + int bytesRead = 0; + while ((bytesRead = stream.read(buffer, 0, 8192)) != -1) { + baos.write(buffer, 0, bytesRead); + } + data = new String(baos.toByteArray()); + } + else { + data = new String("The file is greater than 4MB, " + + " and has not been written to stream." + + " File Size: " + file.getFileSize() + " bytes. This is a" + + " limitation of this particular web application, hard-coded" + + " in org.apache.struts.upload.UploadAction"); + } + } + else { + //write the file to the file specified + OutputStream bos = new FileOutputStream(theForm.getFilePath()); + int bytesRead = 0; + byte[] buffer = new byte[8192]; + while ((bytesRead = stream.read(buffer, 0, 8192)) != -1) { + bos.write(buffer, 0, bytesRead); + } + bos.close(); + data = "The file has been written to \"" + theForm.getFilePath() + "\""; + } } catch (FileNotFoundException fnfe) { return null; 1.2 +45 -3 jakarta-struts/src/upload/org/apache/struts/webapp/upload/UploadForm.java Index: UploadForm.java =================================================================== RCS file: /home/cvs/jakarta-struts/src/upload/org/apache/struts/webapp/upload/UploadForm.java,v retrieving revision 1.1 retrieving revision 1.2 diff -u -r1.1 -r1.2 --- UploadForm.java 2001/03/22 13:17:10 1.1 +++ UploadForm.java 2001/04/11 22:56:24 1.2 @@ -1,7 +1,7 @@ /* - * $Header: /home/cvs/jakarta-struts/src/upload/org/apache/struts/webapp/upload/UploadForm.java,v 1.1 2001/03/22 13:17:10 rleland Exp $ - * $Revision: 1.1 $ - * $Date: 2001/03/22 13:17:10 $ + * $Header: /home/cvs/jakarta-struts/src/upload/org/apache/struts/webapp/upload/UploadForm.java,v 1.2 2001/04/11 22:56:24 mschachter Exp $ + * $Revision: 1.2 $ + * $Date: 2001/04/11 22:56:24 $ * * ==================================================================== * @@ -74,7 +74,7 @@ * that struts uses is org.apache.struts.upload.DiskMultipartRequestHandler. * * @author Mike Schachter - * @version $Revision: 1.1 $ $Date: 2001/03/22 13:17:10 $ + * @version $Revision: 1.2 $ $Date: 2001/04/11 22:56:24 $ */ public class UploadForm extends ActionForm { @@ -83,11 +83,21 @@ * The value of the text the user has sent as form data */ protected String theText; + + /** + * Whether or not to write to a file + */ + protected boolean writeFile; /** * The file that the user has uploaded */ protected FormFile theFile; + + /** + * The file path to write to + */ + protected String filePath; @@ -117,5 +127,37 @@ */ public void setTheFile(FormFile theFile) { this.theFile = theFile; + } + + /** + * Set whether or not to write to a file + */ + public void setWriteFile(boolean writeFile) { + this.writeFile = writeFile; + } + + /** + * Get whether or not to write to a file + */ + public boolean getWriteFile() { + return writeFile; + } + + /** + * Set the path to write a file to + */ + public void setFilePath(String filePath) { + this.filePath = filePath; + } + + /** + * Get the path to write a file to + */ + public String getFilePath() { + return filePath; + } + + public void reset() { + writeFile = false; } } 1.2 +14 -14 jakarta-struts/web/upload/display.jsp Index: display.jsp =================================================================== RCS file: /home/cvs/jakarta-struts/web/upload/display.jsp,v retrieving revision 1.1 retrieving revision 1.2 diff -u -r1.1 -r1.2 --- display.jsp 2000/12/19 19:23:05 1.1 +++ display.jsp 2001/04/11 22:56:27 1.2 @@ -1,15 +1,15 @@ -<%@ page language="java" %> - -<b>The Text</b>: <%= request.getAttribute("text") %> <br /> - -<b>The File name</b>: <%= request.getAttribute("fileName") %> <br /> - -<b>The File content type</b>: <%= request.getAttribute("contentType") %> <br /> - -<b>The File size</b>: <%= request.getAttribute("size") %> <br /> - -<b>The File data</b>: <br /> - -<hr /> -<pre><%= request.getAttribute("data") %></pre> +<%@ page language="java" %> + +<b>The Text</b>: <%= request.getAttribute("text") %> <br /> + +<b>The File name</b>: <%= request.getAttribute("fileName") %> <br /> + +<b>The File content type</b>: <%= request.getAttribute("contentType") %> <br /> + +<b>The File size</b>: <%= request.getAttribute("size") %> <br /> + +<b>The File data</b>: <br /> + +<hr /> +<pre><%= request.getAttribute("data") %></pre> <hr /> 1.3 +6 -0 jakarta-struts/web/upload/upload.jsp Index: upload.jsp =================================================================== RCS file: /home/cvs/jakarta-struts/web/upload/upload.jsp,v retrieving revision 1.2 retrieving revision 1.3 diff -u -r1.2 -r1.3 --- upload.jsp 2001/01/07 00:16:29 1.2 +++ upload.jsp 2001/04/11 22:56:28 1.3 @@ -15,6 +15,12 @@ Please select the file that you would like to upload:<br /> <html:file property="theFile" /><br /><br /> + + If you would rather write this file to another file, please check here: + <html:checkbox property="writeFile" /><br /><br /> + + If you checked the box to write to a file, please specify the file path here:<br /> + <html:text property="filePath" /><br /><br /> <html:submit />