001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.cocoon.servlet.multipart;
018:
019: import java.io.BufferedInputStream;
020: import java.io.ByteArrayInputStream;
021: import java.io.ByteArrayOutputStream;
022: import java.io.File;
023: import java.io.FileOutputStream;
024: import java.io.IOException;
025: import java.io.InputStream;
026: import java.io.OutputStream;
027: import java.io.PushbackInputStream;
028: import java.util.Enumeration;
029: import java.util.Hashtable;
030: import java.util.StringTokenizer;
031: import java.util.Vector;
032:
033: import javax.servlet.http.HttpServletRequest;
034: import javax.servlet.http.HttpSession;
035:
036: import org.apache.cocoon.util.NullOutputStream;
037:
038: /**
039: * This class is used to implement a multipart request wrapper.
040: * It will parse the http post stream and and fill it's hashtable with values.
041: *
042: * The hashtable will contain:
043: * Vector: inline part values
044: * FilePart: file part
045: *
046: * @author <a href="mailto:j.tervoorde@home.nl">Jeroen ter Voorde</a>
047: * @version CVS $Id: MultipartParser.java 479303 2006-11-26 06:56:17Z antonio $
048: */
049: public class MultipartParser {
050:
051: public static final String UPLOAD_STATUS_SESSION_ATTR = "org.apache.cocoon.servlet.multipartparser.status";
052:
053: private final static int FILE_BUFFER_SIZE = 4096;
054:
055: private static final int MAX_BOUNDARY_SIZE = 128;
056:
057: private boolean saveUploadedFilesToDisk;
058:
059: private File uploadDirectory = null;
060:
061: private boolean allowOverwrite;
062:
063: private boolean silentlyRename;
064:
065: private int maxUploadSize;
066:
067: private String characterEncoding;
068:
069: private Hashtable parts;
070:
071: private boolean oversized = false;
072:
073: private int contentLength;
074:
075: private HttpSession session;
076:
077: private boolean hasSession;
078:
079: private Hashtable uploadStatus;
080:
081: /**
082: * Constructor, parses given request
083: *
084: * @param saveUploadedFilesToDisk Write fileparts to the uploadDirectory. If true the corresponding object
085: * in the hashtable will contain a FilePartFile, if false a FilePartArray
086: * @param uploadDirectory The directory to write to if saveUploadedFilesToDisk is true.
087: * @param allowOverwrite Allow existing files to be overwritten.
088: * @param silentlyRename If file exists rename file (using filename+number).
089: * @param maxUploadSize The maximum content length accepted.
090: * @param characterEncoding The character encoding to be used.
091: */
092: public MultipartParser(boolean saveUploadedFilesToDisk,
093: File uploadDirectory, boolean allowOverwrite,
094: boolean silentlyRename, int maxUploadSize,
095: String characterEncoding) {
096: this .saveUploadedFilesToDisk = saveUploadedFilesToDisk;
097: this .uploadDirectory = uploadDirectory;
098: this .allowOverwrite = allowOverwrite;
099: this .silentlyRename = silentlyRename;
100: this .maxUploadSize = maxUploadSize;
101: this .characterEncoding = characterEncoding;
102: }
103:
104: private void parseParts(int contentLength, String contentType,
105: InputStream requestStream) throws IOException,
106: MultipartException {
107: this .contentLength = contentLength;
108: if (contentLength > this .maxUploadSize) {
109: this .oversized = true;
110: }
111:
112: BufferedInputStream bufferedStream = new BufferedInputStream(
113: requestStream);
114: PushbackInputStream pushbackStream = new PushbackInputStream(
115: bufferedStream, MAX_BOUNDARY_SIZE);
116: TokenStream stream = new TokenStream(pushbackStream);
117:
118: parseMultiPart(stream, getBoundary(contentType));
119:
120: }
121:
122: public Hashtable getParts(int contentLength, String contentType,
123: InputStream requestStream) throws IOException,
124: MultipartException {
125: this .parts = new Hashtable();
126: parseParts(contentLength, contentType, requestStream);
127: return this .parts;
128: }
129:
130: public Hashtable getParts(HttpServletRequest request)
131: throws IOException, MultipartException {
132: this .parts = new Hashtable();
133:
134: // Copy all parameters coming from the request URI to the parts table.
135: // This happens when a form's action attribute has some parameters
136: Enumeration names = request.getParameterNames();
137: while (names.hasMoreElements()) {
138: String name = (String) names.nextElement();
139: String[] values = request.getParameterValues(name);
140: Vector v = new Vector(values.length);
141: for (int i = 0; i < values.length; i++) {
142: v.add(values[i]);
143: }
144: this .parts.put(name, v);
145: }
146:
147: // upload progress bar support
148: this .session = request.getSession();
149: this .hasSession = this .session != null;
150: if (this .hasSession) {
151: this .uploadStatus = new Hashtable();
152: this .uploadStatus.put("started", Boolean.FALSE);
153: this .uploadStatus.put("finished", Boolean.FALSE);
154: this .uploadStatus.put("sent", new Integer(0));
155: this .uploadStatus.put("total", new Integer(request
156: .getContentLength()));
157: this .uploadStatus.put("filename", "");
158: this .uploadStatus.put("error", Boolean.FALSE);
159: this .uploadStatus.put("uploadsdone", new Integer(0));
160: this .session.setAttribute(UPLOAD_STATUS_SESSION_ATTR,
161: this .uploadStatus);
162: }
163:
164: parseParts(request.getContentLength(),
165: request.getContentType(), request.getInputStream());
166:
167: if (this .hasSession) {
168: this .uploadStatus.put("finished", Boolean.TRUE);
169: }
170:
171: return this .parts;
172: }
173:
174: /**
175: * Parse a multipart block
176: *
177: * @param ts
178: * @param boundary
179: *
180: * @throws IOException
181: * @throws MultipartException
182: */
183: private void parseMultiPart(TokenStream ts, String boundary)
184: throws IOException, MultipartException {
185:
186: ts.setBoundary(boundary.getBytes());
187: ts.read(); // read first boundary away
188: ts.setBoundary(("\r\n" + boundary).getBytes());
189:
190: while (ts.getState() == TokenStream.STATE_NEXTPART) {
191: ts.nextPart();
192: parsePart(ts);
193: }
194:
195: if (ts.getState() != TokenStream.STATE_ENDMULTIPART) { // sanity check
196: throw new MultipartException("Malformed stream");
197: }
198: }
199:
200: /**
201: * Parse a single part
202: *
203: * @param ts
204: *
205: * @throws IOException
206: * @throws MultipartException
207: */
208: private void parsePart(TokenStream ts) throws IOException,
209: MultipartException {
210:
211: Hashtable headers = new Hashtable();
212: headers = readHeaders(ts);
213: try {
214: if (headers.containsKey("filename")) {
215: if (!"".equals(headers.get("filename"))) {
216: parseFilePart(ts, headers);
217: } else {
218: // IE6 sends an empty part with filename="" for
219: // empty upload fields. Just parse away the part
220: byte[] buf = new byte[32];
221: while (ts.getState() == TokenStream.STATE_READING)
222: ts.read(buf);
223: }
224: } else if (((String) headers.get("content-disposition"))
225: .toLowerCase().equals("form-data")) {
226: parseInlinePart(ts, headers);
227: }
228:
229: // FIXME: multipart/mixed parts are untested.
230: else if (((String) headers.get("content-disposition"))
231: .toLowerCase().indexOf("multipart") > -1) {
232: parseMultiPart(new TokenStream(ts, MAX_BOUNDARY_SIZE),
233: "--" + (String) headers.get("boundary"));
234: ts.read(); // read past boundary
235: } else {
236: throw new MultipartException("Unknown part type");
237: }
238: } catch (IOException e) {
239: throw new MultipartException("Malformed stream: "
240: + e.getMessage());
241: } catch (NullPointerException e) {
242: e.printStackTrace();
243: throw new MultipartException("Malformed header");
244: }
245: }
246:
247: /**
248: * Parse a file part
249: *
250: * @param in
251: * @param headers
252: *
253: * @throws IOException
254: * @throws MultipartException
255: */
256: private void parseFilePart(TokenStream in, Hashtable headers)
257: throws IOException, MultipartException {
258:
259: byte[] buf = new byte[FILE_BUFFER_SIZE];
260: OutputStream out;
261: File file = null;
262:
263: if (oversized) {
264: out = new NullOutputStream();
265: } else if (!saveUploadedFilesToDisk) {
266: out = new ByteArrayOutputStream();
267: } else {
268: String fileName = (String) headers.get("filename");
269: if (File.separatorChar == '\\')
270: fileName = fileName.replace('/', '\\');
271: else
272: fileName = fileName.replace('\\', '/');
273:
274: String filePath = uploadDirectory.getPath()
275: + File.separator;
276: fileName = new File(fileName).getName();
277: file = new File(filePath + fileName);
278:
279: if (!allowOverwrite && !file.createNewFile()) {
280: if (silentlyRename) {
281: int c = 0;
282: do {
283: file = new File(filePath + c++ + "_" + fileName);
284: } while (!file.createNewFile());
285: } else {
286: throw new MultipartException("Duplicate file '"
287: + file.getName() + "' in '"
288: + file.getParent() + "'");
289: }
290: }
291:
292: out = new FileOutputStream(file);
293: }
294:
295: if (hasSession) { // upload widget support
296: this .uploadStatus.put("finished", Boolean.FALSE);
297: this .uploadStatus.put("started", Boolean.TRUE);
298: this .uploadStatus.put("widget", headers.get("name"));
299: this .uploadStatus.put("filename", headers.get("filename"));
300: }
301:
302: int length = 0; // Track length for OversizedPart
303: try {
304: int read = 0;
305: while (in.getState() == TokenStream.STATE_READING) { // read data
306: read = in.read(buf);
307: length += read;
308: out.write(buf, 0, read);
309:
310: if (this .hasSession) {
311: this .uploadStatus.put("sent", new Integer(
312: ((Integer) this .uploadStatus.get("sent"))
313: .intValue()
314: + read));
315: }
316: }
317: if (this .hasSession) { // upload widget support
318: this .uploadStatus.put("uploadsdone",
319: new Integer(((Integer) this .uploadStatus
320: .get("uploadsdone")).intValue() + 1));
321: this .uploadStatus.put("error", Boolean.FALSE);
322: }
323: } catch (IOException ioe) {
324: // don't let incomplete file uploads pile up in the upload dir.
325: // this usually happens with aborted form submits containing very large files.
326: out.close();
327: out = null;
328: if (file != null)
329: file.delete();
330: if (this .hasSession) { // upload widget support
331: this .uploadStatus.put("error", Boolean.TRUE);
332: }
333: throw ioe;
334: } finally {
335: if (out != null)
336: out.close();
337: }
338:
339: String name = (String) headers.get("name");
340: if (oversized) {
341: this .parts.put(name, new RejectedPart(headers, length,
342: this .contentLength, this .maxUploadSize));
343: } else if (file == null) {
344: byte[] bytes = ((ByteArrayOutputStream) out).toByteArray();
345: this .parts.put(name, new PartInMemory(headers,
346: new ByteArrayInputStream(bytes), bytes.length));
347: } else {
348: this .parts.put(name, new PartOnDisk(headers, file));
349: }
350: }
351:
352: /**
353: * Parse an inline part
354: *
355: * @param in
356: * @param headers
357: *
358: * @throws IOException
359: */
360: private void parseInlinePart(TokenStream in, Hashtable headers)
361: throws IOException {
362:
363: // Buffer incoming bytes for proper string decoding (there can be multibyte chars)
364: ByteArrayOutputStream bos = new ByteArrayOutputStream();
365:
366: while (in.getState() == TokenStream.STATE_READING) {
367: int c = in.read();
368: if (c != -1)
369: bos.write(c);
370: }
371:
372: String field = (String) headers.get("name");
373: Vector v = (Vector) this .parts.get(field);
374:
375: if (v == null) {
376: v = new Vector();
377: this .parts.put(field, v);
378: }
379:
380: v.add(new String(bos.toByteArray(), this .characterEncoding));
381: }
382:
383: /**
384: * Read part headers
385: *
386: * @param in
387: *
388: * @throws IOException
389: */
390: private Hashtable readHeaders(TokenStream in) throws IOException {
391:
392: Hashtable headers = new Hashtable();
393: String hdrline = readln(in);
394:
395: while (!"".equals(hdrline)) {
396: StringTokenizer tokenizer = new StringTokenizer(hdrline);
397:
398: headers.put(tokenizer.nextToken(" :").toLowerCase(),
399: tokenizer.nextToken(" :;"));
400:
401: // The extra tokenizer.hasMoreTokens() in headers.put
402: // handles the filename="" case IE6 submits for an empty
403: // upload field.
404: while (tokenizer.hasMoreTokens()) {
405: headers.put(tokenizer.nextToken(" ;=\""), tokenizer
406: .hasMoreTokens() ? tokenizer.nextToken("=\"")
407: : "");
408: }
409:
410: hdrline = readln(in);
411: }
412:
413: return headers;
414: }
415:
416: /**
417: * Get boundary from contentheader
418: *
419: * @param hdr
420: */
421: private String getBoundary(String hdr) {
422:
423: int start = hdr.toLowerCase().indexOf("boundary=");
424: if (start > -1) {
425: return "--" + hdr.substring(start + 9);
426: } else {
427: return null;
428: }
429: }
430:
431: /**
432: * Read string until newline or end of stream
433: *
434: * @param in
435: *
436: * @throws IOException
437: */
438: private String readln(TokenStream in) throws IOException {
439:
440: ByteArrayOutputStream bos = new ByteArrayOutputStream();
441:
442: int b = in.read();
443:
444: while ((b != -1) && (b != '\r')) {
445: bos.write(b);
446: b = in.read();
447: }
448:
449: if (b == '\r') {
450: in.read(); // read '\n'
451: }
452:
453: return new String(bos.toByteArray(), this.characterEncoding);
454: }
455: }
|