1. /*
  2. * $Header: /home/cvs/jakarta-commons/fileupload/src/java/org/apache/commons/fileupload/FileUploadBase.java,v 1.3 2003/06/01 00:18:13 martinc Exp $
  3. * $Revision: 1.3 $
  4. * $Date: 2003/06/01 00:18:13 $
  5. *
  6. * ====================================================================
  7. *
  8. * The Apache Software License, Version 1.1
  9. *
  10. * Copyright (c) 2001-2003 The Apache Software Foundation. All rights
  11. * reserved.
  12. *
  13. * Redistribution and use in source and binary forms, with or without
  14. * modification, are permitted provided that the following conditions
  15. * are met:
  16. *
  17. * 1. Redistributions of source code must retain the above copyright
  18. * notice, this list of conditions and the following disclaimer.
  19. *
  20. * 2. Redistributions in binary form must reproduce the above copyright
  21. * notice, this list of conditions and the following disclaimer in
  22. * the documentation and/or other materials provided with the
  23. * distribution.
  24. *
  25. * 3. The end-user documentation included with the redistribution, if
  26. * any, must include the following acknowlegement:
  27. * "This product includes software developed by the
  28. * Apache Software Foundation (http://www.apache.org/)."
  29. * Alternately, this acknowlegement may appear in the software itself,
  30. * if and wherever such third-party acknowlegements normally appear.
  31. *
  32. * 4. The names "The Jakarta Project", "Commons", and "Apache Software
  33. * Foundation" must not be used to endorse or promote products derived
  34. * from this software without prior written permission. For written
  35. * permission, please contact apache@apache.org.
  36. *
  37. * 5. Products derived from this software may not be called "Apache"
  38. * nor may "Apache" appear in their names without prior written
  39. * permission of the Apache Group.
  40. *
  41. * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
  42. * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
  43. * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  44. * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
  45. * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  46. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  47. * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
  48. * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  49. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  50. * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
  51. * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  52. * SUCH DAMAGE.
  53. * ====================================================================
  54. *
  55. * This software consists of voluntary contributions made by many
  56. * individuals on behalf of the Apache Software Foundation. For more
  57. * information on the Apache Software Foundation, please see
  58. * <http://www.apache.org/>.
  59. *
  60. */
  61. package org.apache.commons.fileupload;
  62. import java.io.IOException;
  63. import java.io.InputStream;
  64. import java.io.OutputStream;
  65. import java.util.ArrayList;
  66. import java.util.HashMap;
  67. import java.util.List;
  68. import java.util.Map;
  69. import javax.servlet.http.HttpServletRequest;
  70. /**
  71. * <p>High level API for processing file uploads.</p>
  72. *
  73. * <p>This class handles multiple files per single HTML widget, sent using
  74. * <code>multipart/mixed</code> encoding type, as specified by
  75. * <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>. Use {@link
  76. * #parseRequest(HttpServletRequest)} to acquire a list of {@link
  77. * org.apache.commons.fileupload.FileItem}s associated with a given HTML
  78. * widget.</p>
  79. *
  80. * <p>How the data for individual parts is stored is determined by the factory
  81. * used to create them; a given part may be in memory, on disk, or somewhere
  82. * else.</p>
  83. *
  84. * @author <a href="mailto:Rafal.Krzewski@e-point.pl">Rafal Krzewski</a>
  85. * @author <a href="mailto:dlr@collab.net">Daniel Rall</a>
  86. * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
  87. * @author <a href="mailto:jmcnally@collab.net">John McNally</a>
  88. * @author <a href="mailto:martinc@apache.org">Martin Cooper</a>
  89. * @author Sean C. Sullivan
  90. *
  91. * @version $Id: FileUploadBase.java,v 1.3 2003/06/01 00:18:13 martinc Exp $
  92. */
  93. public abstract class FileUploadBase
  94. {
  95. // ---------------------------------------------------------- Class methods
  96. /**
  97. * Utility method that determines whether the request contains multipart
  98. * content.
  99. *
  100. * @param req The servlet request to be evaluated. Must be non-null.
  101. *
  102. * @return <code>true</code> if the request is multipart;
  103. * <code>false</code> otherwise.
  104. */
  105. public static final boolean isMultipartContent(HttpServletRequest req)
  106. {
  107. String contentType = req.getHeader(CONTENT_TYPE);
  108. if (contentType == null)
  109. {
  110. return false;
  111. }
  112. if (contentType.startsWith(MULTIPART))
  113. {
  114. return true;
  115. }
  116. return false;
  117. }
  118. // ----------------------------------------------------- Manifest constants
  119. /**
  120. * HTTP content type header name.
  121. */
  122. public static final String CONTENT_TYPE = "Content-type";
  123. /**
  124. * HTTP content disposition header name.
  125. */
  126. public static final String CONTENT_DISPOSITION = "Content-disposition";
  127. /**
  128. * Content-disposition value for form data.
  129. */
  130. public static final String FORM_DATA = "form-data";
  131. /**
  132. * Content-disposition value for file attachment.
  133. */
  134. public static final String ATTACHMENT = "attachment";
  135. /**
  136. * Part of HTTP content type header.
  137. */
  138. public static final String MULTIPART = "multipart/";
  139. /**
  140. * HTTP content type header for multipart forms.
  141. */
  142. public static final String MULTIPART_FORM_DATA = "multipart/form-data";
  143. /**
  144. * HTTP content type header for multiple uploads.
  145. */
  146. public static final String MULTIPART_MIXED = "multipart/mixed";
  147. /**
  148. * The maximum length of a single header line that will be parsed
  149. * (1024 bytes).
  150. */
  151. public static final int MAX_HEADER_SIZE = 1024;
  152. // ----------------------------------------------------------- Data members
  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. * The content encoding to use when reading part headers.
  160. */
  161. private String headerEncoding;
  162. // ----------------------------------------------------- Property accessors
  163. /**
  164. * Returns the factory class used when creating file items.
  165. *
  166. * @return The factory class for new file items.
  167. */
  168. public abstract FileItemFactory getFileItemFactory();
  169. /**
  170. * Sets the factory class to use when creating file items.
  171. *
  172. * @param factory The factory class for new file items.
  173. */
  174. public abstract void setFileItemFactory(FileItemFactory factory);
  175. /**
  176. * Returns the maximum allowed upload size.
  177. *
  178. * @return The maximum allowed size, in bytes.
  179. *
  180. * @see #setSizeMax(long)
  181. *
  182. */
  183. public long getSizeMax()
  184. {
  185. return sizeMax;
  186. }
  187. /**
  188. * Sets the maximum allowed upload size. If negative, there is no maximum.
  189. *
  190. * @param sizeMax The maximum allowed size, in bytes, or -1 for no maximum.
  191. *
  192. * @see #getSizeMax()
  193. *
  194. */
  195. public void setSizeMax(long sizeMax)
  196. {
  197. this.sizeMax = sizeMax;
  198. }
  199. /**
  200. * Retrieves the character encoding used when reading the headers of an
  201. * individual part. When not specified, or <code>null</code>, the platform
  202. * default encoding is used.
  203. *
  204. * @return The encoding used to read part headers.
  205. */
  206. public String getHeaderEncoding()
  207. {
  208. return headerEncoding;
  209. }
  210. /**
  211. * Specifies the character encoding to be used when reading the headers of
  212. * individual parts. When not specified, or <code>null</code>, the platform
  213. * default encoding is used.
  214. *
  215. * @param encoding The encoding used to read part headers.
  216. */
  217. public void setHeaderEncoding(String encoding)
  218. {
  219. headerEncoding = encoding;
  220. }
  221. // --------------------------------------------------------- Public methods
  222. /**
  223. * Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>
  224. * compliant <code>multipart/form-data</code> stream. If files are stored
  225. * on disk, the path is given by <code>getRepository()</code>.
  226. *
  227. * @param req The servlet request to be parsed.
  228. *
  229. * @return A list of <code>FileItem</code> instances parsed from the
  230. * request, in the order that they were transmitted.
  231. *
  232. * @exception FileUploadException if there are problems reading/parsing
  233. * the request or storing files.
  234. */
  235. public List /* FileItem */ parseRequest(HttpServletRequest req)
  236. throws FileUploadException
  237. {
  238. if (null == req)
  239. {
  240. throw new NullPointerException("req parameter");
  241. }
  242. ArrayList items = new ArrayList();
  243. String contentType = req.getHeader(CONTENT_TYPE);
  244. if ((null == contentType) || (!contentType.startsWith(MULTIPART)))
  245. {
  246. throw new InvalidContentTypeException(
  247. "the request doesn't contain a "
  248. + MULTIPART_FORM_DATA
  249. + " or "
  250. + MULTIPART_MIXED
  251. + " stream, content type header is "
  252. + contentType);
  253. }
  254. int requestSize = req.getContentLength();
  255. if (requestSize == -1)
  256. {
  257. throw new UnknownSizeException(
  258. "the request was rejected because it's size is unknown");
  259. }
  260. if (sizeMax >= 0 && requestSize > sizeMax)
  261. {
  262. throw new SizeLimitExceededException(
  263. "the request was rejected because "
  264. + "it's size exceeds allowed range");
  265. }
  266. try
  267. {
  268. int boundaryIndex = contentType.indexOf("boundary=");
  269. if (boundaryIndex < 0)
  270. {
  271. throw new FileUploadException(
  272. "the request was rejected because "
  273. + "no multipart boundary was found");
  274. }
  275. byte[] boundary = contentType.substring(
  276. boundaryIndex + 9).getBytes();
  277. InputStream input = req.getInputStream();
  278. MultipartStream multi = new MultipartStream(input, boundary);
  279. multi.setHeaderEncoding(headerEncoding);
  280. boolean nextPart = multi.skipPreamble();
  281. while (nextPart)
  282. {
  283. Map headers = parseHeaders(multi.readHeaders());
  284. String fieldName = getFieldName(headers);
  285. if (fieldName != null)
  286. {
  287. String subContentType = getHeader(headers, CONTENT_TYPE);
  288. if (subContentType != null && subContentType
  289. .startsWith(MULTIPART_MIXED))
  290. {
  291. // Multiple files.
  292. byte[] subBoundary =
  293. subContentType.substring(
  294. subContentType
  295. .indexOf("boundary=") + 9).getBytes();
  296. multi.setBoundary(subBoundary);
  297. boolean nextSubPart = multi.skipPreamble();
  298. while (nextSubPart)
  299. {
  300. headers = parseHeaders(multi.readHeaders());
  301. if (getFileName(headers) != null)
  302. {
  303. FileItem item =
  304. createItem(headers, false);
  305. OutputStream os = item.getOutputStream();
  306. try
  307. {
  308. multi.readBodyData(os);
  309. }
  310. finally
  311. {
  312. os.close();
  313. }
  314. items.add(item);
  315. }
  316. else
  317. {
  318. // Ignore anything but files inside
  319. // multipart/mixed.
  320. multi.discardBodyData();
  321. }
  322. nextSubPart = multi.readBoundary();
  323. }
  324. multi.setBoundary(boundary);
  325. }
  326. else
  327. {
  328. if (getFileName(headers) != null)
  329. {
  330. // A single file.
  331. FileItem item = createItem(headers, false);
  332. OutputStream os = item.getOutputStream();
  333. try
  334. {
  335. multi.readBodyData(os);
  336. }
  337. finally
  338. {
  339. os.close();
  340. }
  341. items.add(item);
  342. }
  343. else
  344. {
  345. // A form field.
  346. FileItem item = createItem(headers, true);
  347. OutputStream os = item.getOutputStream();
  348. try
  349. {
  350. multi.readBodyData(os);
  351. }
  352. finally
  353. {
  354. os.close();
  355. }
  356. items.add(item);
  357. }
  358. }
  359. }
  360. else
  361. {
  362. // Skip this part.
  363. multi.discardBodyData();
  364. }
  365. nextPart = multi.readBoundary();
  366. }
  367. }
  368. catch (IOException e)
  369. {
  370. throw new FileUploadException(
  371. "Processing of " + MULTIPART_FORM_DATA
  372. + " request failed. " + e.getMessage());
  373. }
  374. return items;
  375. }
  376. // ------------------------------------------------------ Protected methods
  377. /**
  378. * Retrieves the file name from the <code>Content-disposition</code>
  379. * header.
  380. *
  381. * @param headers A <code>Map</code> containing the HTTP request headers.
  382. *
  383. * @return The file name for the current <code>encapsulation</code>.
  384. */
  385. protected String getFileName(Map /* String, String */ headers)
  386. {
  387. String fileName = null;
  388. String cd = getHeader(headers, CONTENT_DISPOSITION);
  389. if (cd.startsWith(FORM_DATA) || cd.startsWith(ATTACHMENT))
  390. {
  391. int start = cd.indexOf("filename=\"");
  392. int end = cd.indexOf('"', start + 10);
  393. if (start != -1 && end != -1)
  394. {
  395. fileName = cd.substring(start + 10, end).trim();
  396. }
  397. }
  398. return fileName;
  399. }
  400. /**
  401. * Retrieves the field name from the <code>Content-disposition</code>
  402. * header.
  403. *
  404. * @param headers A <code>Map</code> containing the HTTP request headers.
  405. *
  406. * @return The field name for the current <code>encapsulation</code>.
  407. */
  408. protected String getFieldName(Map /* String, String */ headers)
  409. {
  410. String fieldName = null;
  411. String cd = getHeader(headers, CONTENT_DISPOSITION);
  412. if (cd != null && cd.startsWith(FORM_DATA))
  413. {
  414. int start = cd.indexOf("name=\"");
  415. int end = cd.indexOf('"', start + 6);
  416. if (start != -1 && end != -1)
  417. {
  418. fieldName = cd.substring(start + 6, end);
  419. }
  420. }
  421. return fieldName;
  422. }
  423. /**
  424. * Creates a new {@link FileItem} instance.
  425. *
  426. * @param headers A <code>Map</code> containing the HTTP request
  427. * headers.
  428. * @param isFormField Whether or not this item is a form field, as
  429. * opposed to a file.
  430. *
  431. * @return A newly created <code>FileItem</code> instance.
  432. *
  433. * @exception FileUploadException if an error occurs.
  434. */
  435. protected FileItem createItem(Map /* String, String */ headers,
  436. boolean isFormField)
  437. throws FileUploadException
  438. {
  439. return getFileItemFactory().createItem(getFieldName(headers),
  440. getHeader(headers, CONTENT_TYPE),
  441. isFormField,
  442. getFileName(headers));
  443. }
  444. /**
  445. * <p> Parses the <code>header-part</code> and returns as key/value
  446. * pairs.
  447. *
  448. * <p> If there are multiple headers of the same names, the name
  449. * will map to a comma-separated list containing the values.
  450. *
  451. * @param headerPart The <code>header-part</code> of the current
  452. * <code>encapsulation</code>.
  453. *
  454. * @return A <code>Map</code> containing the parsed HTTP request headers.
  455. */
  456. protected Map /* String, String */ parseHeaders(String headerPart)
  457. {
  458. Map headers = new HashMap();
  459. char buffer[] = new char[MAX_HEADER_SIZE];
  460. boolean done = false;
  461. int j = 0;
  462. int i;
  463. String header, headerName, headerValue;
  464. try
  465. {
  466. while (!done)
  467. {
  468. i = 0;
  469. // Copy a single line of characters into the buffer,
  470. // omitting trailing CRLF.
  471. while (i < 2 || buffer[i - 2] != '\r' || buffer[i - 1] != '\n')
  472. {
  473. buffer[i++] = headerPart.charAt(j++);
  474. }
  475. header = new String(buffer, 0, i - 2);
  476. if (header.equals(""))
  477. {
  478. done = true;
  479. }
  480. else
  481. {
  482. if (header.indexOf(':') == -1)
  483. {
  484. // This header line is malformed, skip it.
  485. continue;
  486. }
  487. headerName = header.substring(0, header.indexOf(':'))
  488. .trim().toLowerCase();
  489. headerValue =
  490. header.substring(header.indexOf(':') + 1).trim();
  491. if (getHeader(headers, headerName) != null)
  492. {
  493. // More that one heder of that name exists,
  494. // append to the list.
  495. headers.put(headerName,
  496. getHeader(headers, headerName) + ','
  497. + headerValue);
  498. }
  499. else
  500. {
  501. headers.put(headerName, headerValue);
  502. }
  503. }
  504. }
  505. }
  506. catch (IndexOutOfBoundsException e)
  507. {
  508. // Headers were malformed. continue with all that was
  509. // parsed.
  510. }
  511. return headers;
  512. }
  513. /**
  514. * Returns the header with the specified name from the supplied map. The
  515. * header lookup is case-insensitive.
  516. *
  517. * @param headers A <code>Map</code> containing the HTTP request headers.
  518. * @param name The name of the header to return.
  519. *
  520. * @return The value of specified header, or a comma-separated list if
  521. * there were multiple headers of that name.
  522. */
  523. protected final String getHeader(Map /* String, String */ headers,
  524. String name)
  525. {
  526. return (String) headers.get(name.toLowerCase());
  527. }
  528. /**
  529. * Thrown to indicate that the request is not a multipart request.
  530. */
  531. public static class InvalidContentTypeException
  532. extends FileUploadException
  533. {
  534. /**
  535. * Constructs a <code>InvalidContentTypeException</code> with no
  536. * detail message.
  537. */
  538. public InvalidContentTypeException()
  539. {
  540. super();
  541. }
  542. /**
  543. * Constructs an <code>InvalidContentTypeException</code> with
  544. * the specified detail message.
  545. *
  546. * @param message The detail message.
  547. */
  548. public InvalidContentTypeException(String message)
  549. {
  550. super(message);
  551. }
  552. }
  553. /**
  554. * Thrown to indicate that the request size is not specified.
  555. */
  556. public static class UnknownSizeException
  557. extends FileUploadException
  558. {
  559. /**
  560. * Constructs a <code>UnknownSizeException</code> with no
  561. * detail message.
  562. */
  563. public UnknownSizeException()
  564. {
  565. super();
  566. }
  567. /**
  568. * Constructs an <code>UnknownSizeException</code> with
  569. * the specified detail message.
  570. *
  571. * @param message The detail message.
  572. */
  573. public UnknownSizeException(String message)
  574. {
  575. super(message);
  576. }
  577. }
  578. /**
  579. * Thrown to indicate that the request size exceeds the configured maximum.
  580. */
  581. public static class SizeLimitExceededException
  582. extends FileUploadException
  583. {
  584. /**
  585. * Constructs a <code>SizeExceededException</code> with no
  586. * detail message.
  587. */
  588. public SizeLimitExceededException()
  589. {
  590. super();
  591. }
  592. /**
  593. * Constructs an <code>SizeExceededException</code> with
  594. * the specified detail message.
  595. *
  596. * @param message The detail message.
  597. */
  598. public SizeLimitExceededException(String message)
  599. {
  600. super(message);
  601. }
  602. }
  603. }