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