| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.cocoon.servlet.multipart; |
| |
| import java.io.BufferedInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.io.PushbackInputStream; |
| import java.util.Enumeration; |
| import java.util.Hashtable; |
| import java.util.StringTokenizer; |
| import java.util.Vector; |
| |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpSession; |
| |
| import org.apache.cocoon.util.NullOutputStream; |
| |
| /** |
| * This class is used to implement a multipart request wrapper. |
| * It will parse the http post stream and and fill it's hashtable with values. |
| * |
| * The hashtable will contain: |
| * Vector: inline part values |
| * FilePart: file part |
| * |
| * @author <a href="mailto:j.tervoorde@home.nl">Jeroen ter Voorde</a> |
| * @version CVS $Id$ |
| */ |
| public class MultipartParser { |
| |
| public static final String UPLOAD_STATUS_SESSION_ATTR = "org.apache.cocoon.servlet.multipartparser.status"; |
| |
| private final static int FILE_BUFFER_SIZE = 4096; |
| |
| private static final int MAX_BOUNDARY_SIZE = 128; |
| |
| private boolean saveUploadedFilesToDisk; |
| |
| private File uploadDirectory = null; |
| |
| private boolean allowOverwrite; |
| |
| private boolean silentlyRename; |
| |
| private int maxUploadSize; |
| |
| private String characterEncoding; |
| |
| private Hashtable parts; |
| |
| private boolean oversized = false; |
| |
| private int contentLength; |
| |
| private HttpSession session; |
| |
| private boolean hasSession; |
| |
| private Hashtable uploadStatus; |
| |
| /** |
| * Constructor, parses given request |
| * |
| * @param saveUploadedFilesToDisk Write fileparts to the uploadDirectory. If true the corresponding object |
| * in the hashtable will contain a FilePartFile, if false a FilePartArray |
| * @param uploadDirectory The directory to write to if saveUploadedFilesToDisk is true. |
| * @param allowOverwrite Allow existing files to be overwritten. |
| * @param silentlyRename If file exists rename file (using filename+number). |
| * @param maxUploadSize The maximum content length accepted. |
| * @param characterEncoding The character encoding to be used. |
| */ |
| public MultipartParser(boolean saveUploadedFilesToDisk, |
| File uploadDirectory, |
| boolean allowOverwrite, |
| boolean silentlyRename, |
| int maxUploadSize, |
| String characterEncoding) |
| { |
| this.saveUploadedFilesToDisk = saveUploadedFilesToDisk; |
| this.uploadDirectory = uploadDirectory; |
| this.allowOverwrite = allowOverwrite; |
| this.silentlyRename = silentlyRename; |
| this.maxUploadSize = maxUploadSize; |
| this.characterEncoding = characterEncoding; |
| } |
| |
| private void parseParts(int contentLength, String contentType, InputStream requestStream) |
| throws IOException, MultipartException { |
| this.contentLength = contentLength; |
| if (contentLength > this.maxUploadSize) { |
| this.oversized = true; |
| } |
| |
| BufferedInputStream bufferedStream = new BufferedInputStream(requestStream); |
| PushbackInputStream pushbackStream = new PushbackInputStream(bufferedStream, MAX_BOUNDARY_SIZE); |
| TokenStream stream = new TokenStream(pushbackStream); |
| |
| parseMultiPart(stream, getBoundary(contentType)); |
| } |
| |
| public Hashtable getParts(int contentLength, String contentType, InputStream requestStream) |
| throws IOException, MultipartException { |
| this.parts = new Hashtable(); |
| parseParts(contentLength, contentType, requestStream); |
| return this.parts; |
| } |
| |
| public Hashtable getParts(HttpServletRequest request) throws IOException, MultipartException { |
| this.parts = new Hashtable(); |
| |
| // Copy all parameters coming from the request URI to the parts table. |
| // This happens when a form's action attribute has some parameters |
| Enumeration names = request.getParameterNames(); |
| while(names.hasMoreElements()) { |
| String name = (String)names.nextElement(); |
| String[] values = request.getParameterValues(name); |
| Vector v = new Vector(values.length); |
| for (int i = 0; i < values.length; i++) { |
| v.add(values[i]); |
| } |
| this.parts.put(name, v); |
| } |
| |
| // upload progress bar support |
| this.session = request.getSession(); |
| this.hasSession = this.session != null; |
| if (this.hasSession) { |
| this.uploadStatus = new Hashtable(); |
| this.uploadStatus.put("started", Boolean.FALSE); |
| this.uploadStatus.put("finished", Boolean.FALSE); |
| this.uploadStatus.put("sent", new Integer(0)); |
| this.uploadStatus.put("total", new Integer(request.getContentLength())); |
| this.uploadStatus.put("filename", ""); |
| this.uploadStatus.put("error", Boolean.FALSE); |
| this.uploadStatus.put("uploadsdone", new Integer(0)); |
| this.session.setAttribute(UPLOAD_STATUS_SESSION_ATTR, this.uploadStatus); |
| } |
| |
| parseParts(request.getContentLength(), request.getContentType(), request.getInputStream()); |
| |
| if (this.hasSession) { |
| this.uploadStatus.put("finished", Boolean.TRUE); |
| } |
| |
| return this.parts; |
| } |
| |
| /** |
| * Parse a multipart block |
| * |
| * @param ts |
| * @param boundary |
| * |
| * @throws IOException |
| * @throws MultipartException |
| */ |
| private void parseMultiPart(TokenStream ts, String boundary) |
| throws IOException, MultipartException { |
| |
| ts.setBoundary(boundary.getBytes()); |
| ts.read(); // read first boundary away |
| ts.setBoundary(("\r\n" + boundary).getBytes()); |
| |
| while (ts.getState() == TokenStream.STATE_NEXTPART) { |
| ts.nextPart(); |
| parsePart(ts); |
| } |
| |
| if (ts.getState() != TokenStream.STATE_ENDMULTIPART) { // sanity check |
| throw new MultipartException("Malformed stream"); |
| } |
| } |
| |
| /** |
| * Parse a single part |
| * |
| * @param ts |
| * |
| * @throws IOException |
| * @throws MultipartException |
| */ |
| private void parsePart(TokenStream ts) |
| throws IOException, MultipartException { |
| |
| Hashtable headers = new Hashtable(); |
| headers = readHeaders(ts); |
| try { |
| if (headers.containsKey("filename")) { |
| if (!"".equals(headers.get("filename"))) { |
| parseFilePart(ts, headers); |
| } else { |
| // IE6 sends an empty part with filename="" for |
| // empty upload fields. Just parse away the part |
| byte[] buf = new byte[32]; |
| while(ts.getState() == TokenStream.STATE_READING) |
| ts.read(buf); |
| } |
| } else if (((String) headers.get("content-disposition")) |
| .toLowerCase().equals("form-data")) { |
| parseInlinePart(ts, headers); |
| } |
| |
| // FIXME: multipart/mixed parts are untested. |
| else if (((String) headers.get("content-disposition")).toLowerCase() |
| .indexOf("multipart") > -1) { |
| parseMultiPart(new TokenStream(ts, MAX_BOUNDARY_SIZE), |
| "--" + (String) headers.get("boundary")); |
| ts.read(); // read past boundary |
| } else { |
| throw new MultipartException("Unknown part type"); |
| } |
| } catch (IOException e) { |
| throw new MultipartException("Malformed stream: " + e.getMessage()); |
| } catch (NullPointerException e) { |
| e.printStackTrace(); |
| throw new MultipartException("Malformed header"); |
| } |
| } |
| |
| /** |
| * Parse a file part |
| * |
| * @param in |
| * @param headers |
| * |
| * @throws IOException |
| * @throws MultipartException |
| */ |
| private void parseFilePart(TokenStream in, Hashtable headers) |
| throws IOException, MultipartException { |
| |
| byte[] buf = new byte[FILE_BUFFER_SIZE]; |
| OutputStream out; |
| File file = null; |
| |
| if (oversized) { |
| out = new NullOutputStream(); |
| } else if (!saveUploadedFilesToDisk) { |
| out = new ByteArrayOutputStream(); |
| } else { |
| String fileName = (String) headers.get("filename"); |
| if(File.separatorChar == '\\') |
| fileName = fileName.replace('/','\\'); |
| else |
| fileName = fileName.replace('\\','/'); |
| |
| String filePath = uploadDirectory.getPath() + File.separator; |
| fileName = new File(fileName).getName(); |
| file = new File(filePath + fileName); |
| |
| if (!allowOverwrite && !file.createNewFile()) { |
| if (silentlyRename) { |
| int c = 0; |
| do { |
| file = new File(filePath + c++ + "_" + fileName); |
| } while (!file.createNewFile()); |
| } else { |
| throw new MultipartException("Duplicate file '" + file.getName() |
| + "' in '" + file.getParent() + "'"); |
| } |
| } |
| |
| out = new FileOutputStream(file); |
| } |
| |
| if (hasSession) { // upload widget support |
| this.uploadStatus.put("finished", Boolean.FALSE); |
| this.uploadStatus.put("started", Boolean.TRUE); |
| this.uploadStatus.put("widget", headers.get("name")); |
| this.uploadStatus.put("filename", headers.get("filename")); |
| } |
| |
| int length = 0; // Track length for OversizedPart |
| try { |
| int read = 0; |
| while (in.getState() == TokenStream.STATE_READING) { |
| // read data |
| read = in.read(buf); |
| length += read; |
| out.write(buf, 0, read); |
| |
| if (this.hasSession) { |
| this.uploadStatus.put("sent", |
| new Integer(((Integer)this.uploadStatus.get("sent")).intValue() + read) |
| ); |
| } |
| } |
| if (this.hasSession) { // upload widget support |
| this.uploadStatus.put("uploadsdone", |
| new Integer(((Integer)this.uploadStatus.get("uploadsdone")).intValue() + 1) |
| ); |
| this.uploadStatus.put("error", Boolean.FALSE); |
| } |
| } catch (IOException ioe) { |
| // don't let incomplete file uploads pile up in the upload dir. |
| // this usually happens with aborted form submits containing very large files. |
| out.close(); |
| out = null; |
| if ( file!=null ) file.delete(); |
| if (this.hasSession) { // upload widget support |
| this.uploadStatus.put("error", Boolean.TRUE); |
| } |
| throw ioe; |
| } finally { |
| if ( out!=null ) out.close(); |
| } |
| |
| String field = (String)headers.get("name"); |
| Vector v = (Vector) this.parts.get(field); |
| |
| if (v == null) { |
| v = new Vector(); |
| this.parts.put(field, v); |
| } |
| |
| if (oversized) { |
| v.add(new RejectedPart(headers, length, this.contentLength, this.maxUploadSize)); |
| } else if (file == null) { |
| byte[] bytes = ((ByteArrayOutputStream) out).toByteArray(); |
| v.add(new PartInMemory(headers, bytes)); |
| } else { |
| v.add(new PartOnDisk(headers, file)); |
| } |
| } |
| |
| /** |
| * Parse an inline part |
| * |
| * @param in |
| * @param headers |
| * |
| * @throws IOException |
| */ |
| private void parseInlinePart(TokenStream in, Hashtable headers) |
| throws IOException { |
| |
| // Buffer incoming bytes for proper string decoding (there can be multibyte chars) |
| ByteArrayOutputStream bos = new ByteArrayOutputStream(); |
| |
| while (in.getState() == TokenStream.STATE_READING) { |
| int c = in.read(); |
| if (c != -1) bos.write(c); |
| } |
| |
| String field = (String) headers.get("name"); |
| Vector v = (Vector) this.parts.get(field); |
| |
| if (v == null) { |
| v = new Vector(); |
| this.parts.put(field, v); |
| } |
| |
| v.add(new String(bos.toByteArray(), this.characterEncoding)); |
| } |
| |
| /** |
| * Read part headers |
| * |
| * @param in |
| * |
| * @throws IOException |
| */ |
| private Hashtable readHeaders(TokenStream in) throws IOException { |
| |
| Hashtable headers = new Hashtable(); |
| String hdrline = readln(in); |
| |
| while (!"".equals(hdrline)) { |
| |
| headers.putAll(readHeaders(hdrline)); |
| |
| hdrline = readln(in); |
| } |
| |
| return headers; |
| } |
| |
| private Hashtable readHeaders(String hdrline) { |
| |
| Hashtable headers = new Hashtable(); |
| |
| boolean inValue = false; |
| boolean inFirstName = true; |
| boolean inName = true; |
| boolean inQuotedString = false; |
| |
| String name = null; |
| String value = null; |
| |
| int start = 0; |
| int offset = 0; |
| |
| for (int i = 0; i < hdrline.length(); i++) { |
| |
| char c = hdrline.charAt(i); |
| |
| switch (c) { |
| case ':': |
| case '=': |
| if (!inQuotedString) { |
| if (inName) { |
| name = hdrline.substring(start, start + offset).trim(); |
| |
| if (inFirstName) { |
| name = name.toLowerCase(); |
| inFirstName = false; |
| } |
| |
| inName = false; |
| inValue = true; |
| start = i + 1; |
| offset = 0; |
| } |
| } |
| else { |
| offset++; |
| } |
| break; |
| case ';': |
| if (!inQuotedString) { |
| if (inValue) { |
| value = hdrline.substring(start, start + offset).trim(); |
| headers.put(name, value); |
| inName = true; |
| inValue = false; |
| start = i + 1; |
| offset = 0; |
| } |
| } |
| else { |
| offset++; |
| } |
| |
| break; |
| case '"': |
| inQuotedString = !inQuotedString; |
| if (inQuotedString) { |
| start = i + 1; |
| } |
| break; |
| default: |
| offset++; |
| break; |
| } |
| } |
| |
| // last part |
| value = hdrline.substring(start, start + offset).trim(); |
| headers.put(name, value); |
| |
| return headers; |
| } |
| |
| /** |
| * Get boundary from contentheader |
| */ |
| private String getBoundary(String hdr) { |
| |
| int start = hdr.toLowerCase().indexOf("boundary="); |
| if (start > -1) { |
| return "--" + hdr.substring(start + 9); |
| } |
| return null; |
| } |
| |
| /** |
| * Read string until newline or end of stream |
| * |
| * @param in |
| * |
| * @throws IOException |
| */ |
| private String readln(TokenStream in) throws IOException { |
| |
| ByteArrayOutputStream bos = new ByteArrayOutputStream(); |
| |
| int b = in.read(); |
| |
| while ((b != -1) && (b != '\r')) { |
| bos.write(b); |
| b = in.read(); |
| } |
| |
| if (b == '\r') { |
| in.read(); // read '\n' |
| } |
| |
| return new String(bos.toByteArray(), this.characterEncoding); |
| } |
| |
| } |