001: /*
002: * Copyright 2001-2005 The Apache Software Foundation
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016: package net.myvietnam.mvncore.web.fileupload;
017:
018: import java.io.IOException;
019: import java.io.InputStream;
020: import java.io.OutputStream;
021: import java.io.UnsupportedEncodingException;
022: import java.util.ArrayList;
023: import java.util.HashMap;
024: import java.util.List;
025: import java.util.Map;
026: import javax.servlet.http.HttpServletRequest;
027:
028: import net.myvietnam.mvncore.web.fileupload.servlet.ServletRequestContext;
029:
030: /**
031: * <p>High level API for processing file uploads.</p>
032: *
033: * <p>This class handles multiple files per single HTML widget, sent using
034: * <code>multipart/mixed</code> encoding type, as specified by
035: * <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>. Use {@link
036: * #parseRequest(HttpServletRequest)} to acquire a list of {@link
037: * org.apache.commons.fileupload.FileItem}s associated with a given HTML
038: * widget.</p>
039: *
040: * <p>How the data for individual parts is stored is determined by the factory
041: * used to create them; a given part may be in memory, on disk, or somewhere
042: * else.</p>
043: *
044: * @author <a href="mailto:Rafal.Krzewski@e-point.pl">Rafal Krzewski</a>
045: * @author <a href="mailto:dlr@collab.net">Daniel Rall</a>
046: * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
047: * @author <a href="mailto:jmcnally@collab.net">John McNally</a>
048: * @author <a href="mailto:martinc@apache.org">Martin Cooper</a>
049: * @author Sean C. Sullivan
050: *
051: * @version $Id: FileUploadBase.java,v 1.4 2006/06/12 07:47:16 minhnn Exp $
052: */
053: public abstract class FileUploadBase {
054:
055: // ---------------------------------------------------------- Class methods
056:
057: /**
058: * <p>Utility method that determines whether the request contains multipart
059: * content.</p>
060: *
061: * <p><strong>NOTE:</strong>This method will be moved to the
062: * <code>ServletFileUpload</code> class after the FileUpload 1.1 release.
063: * Unfortunately, since this method is static, it is not possible to
064: * provide its replacement until this method is removed.</p>
065: *
066: * @param ctx The request context to be evaluated. Must be non-null.
067: *
068: * @return <code>true</code> if the request is multipart;
069: * <code>false</code> otherwise.
070: */
071: public static final boolean isMultipartContent(RequestContext ctx) {
072: String contentType = ctx.getContentType();
073: if (contentType == null) {
074: return false;
075: }
076: if (contentType.toLowerCase().startsWith(MULTIPART)) {
077: return true;
078: }
079: return false;
080: }
081:
082: /**
083: * Utility method that determines whether the request contains multipart
084: * content.
085: *
086: * @param req The servlet request to be evaluated. Must be non-null.
087: *
088: * @return <code>true</code> if the request is multipart;
089: * <code>false</code> otherwise.
090: *
091: * @deprecated Use the method on <code>ServletFileUpload</code> instead.
092: */
093: public static final boolean isMultipartContent(
094: HttpServletRequest req) {
095: if (!"post".equals(req.getMethod().toLowerCase())) {
096: return false;
097: }
098: String contentType = req.getContentType();
099: if (contentType == null) {
100: return false;
101: }
102: if (contentType.toLowerCase().startsWith(MULTIPART)) {
103: return true;
104: }
105: return false;
106: }
107:
108: // ----------------------------------------------------- Manifest constants
109:
110: /**
111: * HTTP content type header name.
112: */
113: public static final String CONTENT_TYPE = "Content-type";
114:
115: /**
116: * HTTP content disposition header name.
117: */
118: public static final String CONTENT_DISPOSITION = "Content-disposition";
119:
120: /**
121: * Content-disposition value for form data.
122: */
123: public static final String FORM_DATA = "form-data";
124:
125: /**
126: * Content-disposition value for file attachment.
127: */
128: public static final String ATTACHMENT = "attachment";
129:
130: /**
131: * Part of HTTP content type header.
132: */
133: public static final String MULTIPART = "multipart/";
134:
135: /**
136: * HTTP content type header for multipart forms.
137: */
138: public static final String MULTIPART_FORM_DATA = "multipart/form-data";
139:
140: /**
141: * HTTP content type header for multiple uploads.
142: */
143: public static final String MULTIPART_MIXED = "multipart/mixed";
144:
145: /**
146: * The maximum length of a single header line that will be parsed
147: * (1024 bytes).
148: */
149: public static final int MAX_HEADER_SIZE = 1024;
150:
151: // ----------------------------------------------------------- Data members
152:
153: /**
154: * The maximum size permitted for an uploaded file. A value of -1 indicates
155: * no maximum.
156: */
157: private long sizeMax = -1;
158:
159: /**
160: * The content encoding to use when reading part headers.
161: */
162: private String headerEncoding;
163:
164: // ----------------------------------------------------- Property accessors
165:
166: /**
167: * Returns the factory class used when creating file items.
168: *
169: * @return The factory class for new file items.
170: */
171: public abstract FileItemFactory getFileItemFactory();
172:
173: /**
174: * Sets the factory class to use when creating file items.
175: *
176: * @param factory The factory class for new file items.
177: */
178: public abstract void setFileItemFactory(FileItemFactory factory);
179:
180: /**
181: * Returns the maximum allowed upload size.
182: *
183: * @return The maximum allowed size, in bytes.
184: *
185: * @see #setSizeMax(long)
186: *
187: */
188: public long getSizeMax() {
189: return sizeMax;
190: }
191:
192: /**
193: * Sets the maximum allowed upload size. If negative, there is no maximum.
194: *
195: * @param sizeMax The maximum allowed size, in bytes, or -1 for no maximum.
196: *
197: * @see #getSizeMax()
198: *
199: */
200: public void setSizeMax(long sizeMax) {
201: this .sizeMax = sizeMax;
202: }
203:
204: /**
205: * Retrieves the character encoding used when reading the headers of an
206: * individual part. When not specified, or <code>null</code>, the request
207: * encoding is used. If that is also not specified, or <code>null</code>,
208: * the platform default encoding is used.
209: *
210: * @return The encoding used to read part headers.
211: */
212: public String getHeaderEncoding() {
213: return headerEncoding;
214: }
215:
216: /**
217: * Specifies the character encoding to be used when reading the headers of
218: * individual part. When not specified, or <code>null</code>, the request
219: * encoding is used. If that is also not specified, or <code>null</code>,
220: * the platform default encoding is used.
221: *
222: * @param encoding The encoding used to read part headers.
223: */
224: public void setHeaderEncoding(String encoding) {
225: headerEncoding = encoding;
226: }
227:
228: // --------------------------------------------------------- Public methods
229:
230: /**
231: * Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>
232: * compliant <code>multipart/form-data</code> stream.
233: *
234: * @param req The servlet request to be parsed.
235: *
236: * @return A list of <code>FileItem</code> instances parsed from the
237: * request, in the order that they were transmitted.
238: *
239: * @throws FileUploadException if there are problems reading/parsing
240: * the request or storing files.
241: *
242: * @deprecated Use the method in <code>ServletFileUpload</code> instead.
243: */
244: public List /* FileItem */parseRequest(HttpServletRequest req)
245: throws FileUploadException {
246: return parseRequest(new ServletRequestContext(req));
247: }
248:
249: /**
250: * Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>
251: * compliant <code>multipart/form-data</code> stream.
252: *
253: * @param ctx The context for the request to be parsed.
254: *
255: * @return A list of <code>FileItem</code> instances parsed from the
256: * request, in the order that they were transmitted.
257: *
258: * @throws FileUploadException if there are problems reading/parsing
259: * the request or storing files.
260: */
261: public List /* FileItem */parseRequest(RequestContext ctx)
262: throws FileUploadException {
263: if (ctx == null) {
264: throw new NullPointerException("ctx parameter");
265: }
266:
267: ArrayList items = new ArrayList();
268: String contentType = ctx.getContentType();
269:
270: if ((null == contentType)
271: || (!contentType.toLowerCase().startsWith(MULTIPART))) {
272: throw new InvalidContentTypeException(
273: "the request doesn't contain a "
274: + MULTIPART_FORM_DATA + " or "
275: + MULTIPART_MIXED
276: + " stream, content type header is "
277: + contentType);
278: }
279: int requestSize = ctx.getContentLength();
280:
281: if (requestSize == -1) {
282: throw new UnknownSizeException(
283: "the request was rejected because its size is unknown");
284: }
285:
286: if (sizeMax >= 0 && requestSize > sizeMax) {
287: throw new SizeLimitExceededException(
288: "the request was rejected because its size ("
289: + requestSize
290: + ") exceeds the configured maximum ("
291: + sizeMax + ")", requestSize, sizeMax);
292: }
293:
294: String charEncoding = headerEncoding;
295: if (charEncoding == null) {
296: charEncoding = ctx.getCharacterEncoding();
297: }
298:
299: try {
300: byte[] boundary = getBoundary(contentType);
301: if (boundary == null) {
302: throw new FileUploadException(
303: "the request was rejected because "
304: + "no multipart boundary was found");
305: }
306:
307: InputStream input = ctx.getInputStream();
308:
309: MultipartStream multi = new MultipartStream(input, boundary);
310: multi.setHeaderEncoding(charEncoding);
311:
312: boolean nextPart = multi.skipPreamble();
313: while (nextPart) {
314: Map headers = parseHeaders(multi.readHeaders());
315: String fieldName = getFieldName(headers);
316: if (fieldName != null) {
317: String subContentType = getHeader(headers,
318: CONTENT_TYPE);
319: if (subContentType != null
320: && subContentType.toLowerCase().startsWith(
321: MULTIPART_MIXED)) {
322: // Multiple files.
323: byte[] subBoundary = getBoundary(subContentType);
324: multi.setBoundary(subBoundary);
325: boolean nextSubPart = multi.skipPreamble();
326: while (nextSubPart) {
327: headers = parseHeaders(multi.readHeaders());
328: if (getFileName(headers) != null) {
329: FileItem item = createItem(headers,
330: false);
331: OutputStream os = item
332: .getOutputStream();
333: try {
334: multi.readBodyData(os);
335: } finally {
336: os.close();
337: }
338: items.add(item);
339: } else {
340: // Ignore anything but files inside
341: // multipart/mixed.
342: multi.discardBodyData();
343: }
344: nextSubPart = multi.readBoundary();
345: }
346: multi.setBoundary(boundary);
347: } else {
348: FileItem item = createItem(headers,
349: getFileName(headers) == null);
350: OutputStream os = item.getOutputStream();
351: try {
352: multi.readBodyData(os);
353: } finally {
354: os.close();
355: }
356: items.add(item);
357: }
358: } else {
359: // Skip this part.
360: multi.discardBodyData();
361: }
362: nextPart = multi.readBoundary();
363: }
364: } catch (IOException e) {
365: throw new FileUploadException("Processing of "
366: + MULTIPART_FORM_DATA + " request failed. "
367: + e.getMessage());
368: }
369:
370: return items;
371: }
372:
373: // ------------------------------------------------------ Protected methods
374:
375: /**
376: * Retrieves the boundary from the <code>Content-type</code> header.
377: *
378: * @param contentType The value of the content type header from which to
379: * extract the boundary value.
380: *
381: * @return The boundary, as a byte array.
382: */
383: protected byte[] getBoundary(String contentType) {
384: ParameterParser parser = new ParameterParser();
385: parser.setLowerCaseNames(true);
386: // Parameter parser can handle null input
387: Map params = parser.parse(contentType, ';');
388: String boundaryStr = (String) params.get("boundary");
389:
390: if (boundaryStr == null) {
391: return null;
392: }
393: byte[] boundary;
394: try {
395: boundary = boundaryStr.getBytes("ISO-8859-1");
396: } catch (UnsupportedEncodingException e) {
397: boundary = boundaryStr.getBytes();
398: }
399: return boundary;
400: }
401:
402: /**
403: * Retrieves the file name from the <code>Content-disposition</code>
404: * header.
405: *
406: * @param headers A <code>Map</code> containing the HTTP request headers.
407: *
408: * @return The file name for the current <code>encapsulation</code>.
409: */
410: protected String getFileName(Map /* String, String */headers) {
411: String fileName = null;
412: String cd = getHeader(headers, CONTENT_DISPOSITION);
413: if (cd != null) {
414: String cdl = cd.toLowerCase();
415: if (cdl.startsWith(FORM_DATA) || cdl.startsWith(ATTACHMENT)) {
416: ParameterParser parser = new ParameterParser();
417: parser.setLowerCaseNames(true);
418: // Parameter parser can handle null input
419: Map params = parser.parse(cd, ';');
420: if (params.containsKey("filename")) {
421: fileName = (String) params.get("filename");
422: if (fileName != null) {
423: fileName = fileName.trim();
424: } else {
425: // Even if there is no value, the parameter is present,
426: // so we return an empty file name rather than no file
427: // name.
428: fileName = "";
429: }
430: }
431: }
432: }
433: return fileName;
434: }
435:
436: /**
437: * Retrieves the field name from the <code>Content-disposition</code>
438: * header.
439: *
440: * @param headers A <code>Map</code> containing the HTTP request headers.
441: *
442: * @return The field name for the current <code>encapsulation</code>.
443: */
444: protected String getFieldName(Map /* String, String */headers) {
445: String fieldName = null;
446: String cd = getHeader(headers, CONTENT_DISPOSITION);
447: if (cd != null && cd.toLowerCase().startsWith(FORM_DATA)) {
448:
449: ParameterParser parser = new ParameterParser();
450: parser.setLowerCaseNames(true);
451: // Parameter parser can handle null input
452: Map params = parser.parse(cd, ';');
453: fieldName = (String) params.get("name");
454: if (fieldName != null) {
455: fieldName = fieldName.trim();
456: }
457: }
458: return fieldName;
459: }
460:
461: /**
462: * Creates a new {@link FileItem} instance.
463: *
464: * @param headers A <code>Map</code> containing the HTTP request
465: * headers.
466: * @param isFormField Whether or not this item is a form field, as
467: * opposed to a file.
468: *
469: * @return A newly created <code>FileItem</code> instance.
470: *
471: * @throws FileUploadException if an error occurs.
472: */
473: protected FileItem createItem(Map /* String, String */headers,
474: boolean isFormField) throws FileUploadException {
475: return getFileItemFactory().createItem(getFieldName(headers),
476: getHeader(headers, CONTENT_TYPE), isFormField,
477: getFileName(headers));
478: }
479:
480: /**
481: * <p> Parses the <code>header-part</code> and returns as key/value
482: * pairs.
483: *
484: * <p> If there are multiple headers of the same names, the name
485: * will map to a comma-separated list containing the values.
486: *
487: * @param headerPart The <code>header-part</code> of the current
488: * <code>encapsulation</code>.
489: *
490: * @return A <code>Map</code> containing the parsed HTTP request headers.
491: */
492: protected Map /* String, String */parseHeaders(String headerPart) {
493: Map headers = new HashMap();
494: char[] buffer = new char[MAX_HEADER_SIZE];
495: boolean done = false;
496: int j = 0;
497: int i;
498: String header, headerName, headerValue;
499: try {
500: while (!done) {
501: i = 0;
502: // Copy a single line of characters into the buffer,
503: // omitting trailing CRLF.
504: while (i < 2 || buffer[i - 2] != '\r'
505: || buffer[i - 1] != '\n') {
506: buffer[i++] = headerPart.charAt(j++);
507: }
508: header = new String(buffer, 0, i - 2);
509: if (header.equals("")) {
510: done = true;
511: } else {
512: if (header.indexOf(':') == -1) {
513: // This header line is malformed, skip it.
514: continue;
515: }
516: headerName = header.substring(0,
517: header.indexOf(':')).trim().toLowerCase();
518: headerValue = header.substring(
519: header.indexOf(':') + 1).trim();
520: if (getHeader(headers, headerName) != null) {
521: // More that one heder of that name exists,
522: // append to the list.
523: headers.put(headerName, getHeader(headers,
524: headerName)
525: + ',' + headerValue);
526: } else {
527: headers.put(headerName, headerValue);
528: }
529: }
530: }
531: } catch (IndexOutOfBoundsException e) {
532: // Headers were malformed. continue with all that was
533: // parsed.
534: }
535: return headers;
536: }
537:
538: /**
539: * Returns the header with the specified name from the supplied map. The
540: * header lookup is case-insensitive.
541: *
542: * @param headers A <code>Map</code> containing the HTTP request headers.
543: * @param name The name of the header to return.
544: *
545: * @return The value of specified header, or a comma-separated list if
546: * there were multiple headers of that name.
547: */
548: protected final String getHeader(Map /* String, String */headers,
549: String name) {
550: return (String) headers.get(name.toLowerCase());
551: }
552:
553: /**
554: * Thrown to indicate that the request is not a multipart request.
555: */
556: public static class InvalidContentTypeException extends
557: FileUploadException {
558: /**
559: * Constructs a <code>InvalidContentTypeException</code> with no
560: * detail message.
561: */
562: public InvalidContentTypeException() {
563: super ();
564: }
565:
566: /**
567: * Constructs an <code>InvalidContentTypeException</code> with
568: * the specified detail message.
569: *
570: * @param message The detail message.
571: */
572: public InvalidContentTypeException(String message) {
573: super (message);
574: }
575: }
576:
577: /**
578: * Thrown to indicate that the request size is not specified.
579: */
580: public static class UnknownSizeException extends
581: FileUploadException {
582: /**
583: * Constructs a <code>UnknownSizeException</code> with no
584: * detail message.
585: */
586: public UnknownSizeException() {
587: super ();
588: }
589:
590: /**
591: * Constructs an <code>UnknownSizeException</code> with
592: * the specified detail message.
593: *
594: * @param message The detail message.
595: */
596: public UnknownSizeException(String message) {
597: super (message);
598: }
599: }
600:
601: /**
602: * Thrown to indicate that the request size exceeds the configured maximum.
603: */
604: public static class SizeLimitExceededException extends
605: FileUploadException {
606: /**
607: * The actual size of the request.
608: */
609: private long actual;
610:
611: /**
612: * The maximum permitted size of the request.
613: */
614: private long permitted;
615:
616: /**
617: * Constructs a <code>SizeExceededException</code> with no
618: * detail message.
619: */
620: public SizeLimitExceededException() {
621: super ();
622: }
623:
624: /**
625: * Constructs a <code>SizeExceededException</code> with
626: * the specified detail message.
627: *
628: * @param message The detail message.
629: */
630: public SizeLimitExceededException(String message) {
631: super (message);
632: }
633:
634: /**
635: * Constructs a <code>SizeExceededException</code> with
636: * the specified detail message, and actual and permitted sizes.
637: *
638: * @param message The detail message.
639: * @param actual The actual request size.
640: * @param permitted The maximum permitted request size.
641: */
642: public SizeLimitExceededException(String message, long actual,
643: long permitted) {
644: super (message);
645: this .actual = actual;
646: this .permitted = permitted;
647: }
648:
649: /**
650: * Retrieves the actual size of the request.
651: *
652: * @return The actual size of the request.
653: */
654: public long getActualSize() {
655: return actual;
656: }
657:
658: /**
659: * Retrieves the permitted size of the request.
660: *
661: * @return The permitted size of the request.
662: */
663: public long getPermittedSize() {
664: return permitted;
665: }
666: }
667:
668: }
|