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