View Javadoc

1   /*
2    * $Id: MultipartIterator.java 127 2004-11-06 10:15:26Z josem $
3    *
4    * Tarsis
5    * Copyright (C) 2002 Talika Open Source Group
6    *
7    * This program is free software; you can redistribute it and/or modify
8    * it under the terms of the GNU General Public License as published by
9    * the Free Software Foundation; either version 2 of the License, or
10   * (at your option) any later version.
11   *
12   * This program is distributed in the hope that it will be useful,
13   * but WITHOUT ANY WARRANTY; without even the implied warranty of
14   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15   * GNU General Public License for more details.
16   *
17   * You should have received a copy of the GNU General Public License
18   * along with this program; if not, write to the Free Software
19   * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20   *
21   */
22  
23  package org.talika.tarsis.filters.upload;
24  
25  import java.io.BufferedInputStream;
26  import java.io.BufferedOutputStream;
27  import java.io.File;
28  import java.io.FileOutputStream;
29  import java.io.IOException;
30  
31  import javax.servlet.ServletRequest;
32  
33  /**
34   * Multipart iterator allows us to iterate multipart request element as like a
35   * <code>java.util.Iterator</code> although it doesn't implement this interface.
36   *
37   * @author  Jose M. Palomar
38   * @version $Revision: 127 $
39   */
40  public final class MultipartIterator {
41  
42      // Constants
43      /**
44       * Default charset encoding.
45       */
46      private static final String DEFAULT_ENCODING = "iso-8859-1";
47  
48      /**
49       * Content type parameter name.
50       */
51      private static final String CONTENT_TYPE = "content-type";
52  
53      /**
54       * "text/plain" content type literal.
55       */
56      private static final String CONTENT_TYPE_TEXT_PLAIN = "text/plain";
57  
58      /**
59       * "application/octet-stream" content type literal.
60       */
61      private static final String CONTENT_TYPE_APPLICATION_OCTET_STREAM
62                                                          = "application/octet-stream";
63      /**
64       * Content disposition parameter name.
65       */
66      private static final String CONTENT_DISPOSITION     = "content-disposition";
67  
68      /**
69       * Default content disposition.
70       */
71      private static final String DEFAULT_CONTENT_DISPOSITION
72                                                          = "form-data";
73      /**
74       * Boundary parameter name.
75       */
76      private static final String PARAMETER_BOUNDARY      = "boundary";
77  
78      /**
79       * Name parameter name.
80       */
81      private static final String PARAMETER_NAME          = "name";
82  
83      /**
84       * Filename parameter name.
85       */
86      private static final String PARAMETER_FILENAME      = "filename";
87  
88      /**
89       * Charset parameter name.
90       */
91      private static final String PARAMETER_CHARSET       = "charset";
92  
93      /**
94       * Double dash.
95       */
96      private static final String DOUBLE_DASH             = "--";
97  
98      /**
99       * Temporal filename prefix.
100      */
101     private static final String TMP_PREFIX              = "trss";
102 
103     /**
104      * Default buffer size.
105      */
106     private static final int DEFAULT_BUFFER_SIZE = 4 * 1024;
107 
108     /**
109      * Default disk buffer size.
110      */
111     private static final int DISK_BUFFER_SIZE = 20 * 1024;
112 
113     /**
114      * Default text buffer size.
115      */
116     private static final int TEXT_BUFFER_SIZE = 1 * 1024;
117 
118     // Fields
119     /**
120      * Client's request.
121      */
122     private ServletRequest servletRequest;
123 
124     /**
125      * Multipart input stream.
126      */
127     private MultipartInputStream inputStream;
128 
129     /**
130      * Request's max size.
131      */
132     private int maxSize;
133 
134     /**
135      * Request's size.
136      */
137     private int size;
138 
139     /**
140      * Buffer size.
141      */
142     private int bufferSize;
143 
144     /**
145      * Temporal directory.
146      */
147     private String tmpDir;
148 
149     /**
150      * Boundary value.
151      */
152     private String boundary;
153 
154     /**
155      * Boundary start value.
156      */
157     private String startBoundary;
158 
159     /**
160      * Boundary end value.
161      */
162     private String endBoundary;
163 
164     /**
165      * Default charset encoding.
166      */
167     private String defaultEncoding;
168 
169     // Constructors
170     /**
171      * Creates a new <code>MultipartIterator</code> using given client's request.
172      *
173      * @param servletRequest ServletRequest client's request.
174      * @param maxSize int max allowad size.
175      * @param bufferSize int buffer size.
176      * @param tmpDir String temporal directory.
177      * @throws MultipartRequestException if there is any error procesing multipart
178      * request.
179      * @throws IOException if there is any I/O error.
180      */
181     public MultipartIterator(ServletRequest servletRequest, int maxSize, int bufferSize,
182     String tmpDir) throws MultipartRequestException, IOException {
183 
184         this.servletRequest = servletRequest;
185         this.maxSize = maxSize;
186         if (bufferSize > DEFAULT_BUFFER_SIZE) {
187             this.bufferSize = bufferSize;
188         }
189         else {
190             this.bufferSize = DEFAULT_BUFFER_SIZE;
191         }
192         this.tmpDir = tmpDir;
193 
194         this.boundary = getBoundary();
195         if (this.boundary == null) {
196             throw new MultipartRequestException("Can't retrieve boundary");
197         }
198         this.startBoundary = DOUBLE_DASH + this.boundary;
199         this.endBoundary = DOUBLE_DASH + this.boundary + DOUBLE_DASH;
200 
201         this.size = getSize();
202         if (this.size > this.maxSize) {
203             throw new MultipartRequestException("Max Content-Length exceeded");
204         }
205 
206         this.defaultEncoding = getEncoding();
207 
208         this.inputStream = new MultipartInputStream(
209                                 new BufferedInputStream(servletRequest.getInputStream(),
210                                                         this.bufferSize),
211                                 this.boundary, this.defaultEncoding);
212 
213     }
214 
215     // Methods
216     /**
217      * Returns multipart request boundary value.
218      *
219      * @return String multipart request boundary value.
220      */
221     private String getBoundary() {
222 
223         String contentType = servletRequest.getContentType();
224         if (contentType != null && contentType.lastIndexOf(PARAMETER_BOUNDARY) != -1) {
225 
226             String boundary =
227                 contentType.substring(contentType.lastIndexOf(PARAMETER_BOUNDARY) + 9);
228             if (boundary.endsWith("\n")) {
229                 boundary = boundary.substring(0, this.boundary.length() - 1);
230             }
231 
232             return boundary;
233 
234         }
235         else {
236             return null;
237         }
238 
239     }
240 
241     /**
242      * Returns multipart request size.
243      *
244      * @return int multipart request size.
245      */
246     private int getSize() {
247         return servletRequest.getContentLength();
248     }
249 
250     /**
251      * Returns multipart request charset encoding.
252      *
253      * @return String multipart request charset encoding.
254      */
255     private String getEncoding() {
256 
257         String encoding = this.servletRequest.getCharacterEncoding();
258         if (encoding == null) {
259             encoding = DEFAULT_ENCODING;
260         }
261 
262         return encoding;
263 
264     }
265 
266     /**
267      * Returns <code>true</code> if the iteration has more elements.
268      *
269      * @return boolean <code>true</code> if the iteration has more elements.
270      * @throws MultipartRequestException if there is any error procesing multipart
271      * request.
272      * @throws IOException if there is any I/O error.
273      */
274     public boolean hasNext() throws MultipartRequestException, IOException {
275         return readBoundary();
276     }
277 
278     /**
279      * Reads boundary from input stream.
280      *
281      * @return boolean <code>true</code> if boundary start was readed;
282      * <code>false</code> if boundary end was readed.
283      * @throws MultipartRequestException if there is any error procesing multipart
284      * request.
285      * @throws IOException if there is any I/O error.
286      */
287     private boolean readBoundary() throws MultipartRequestException, IOException {
288 
289         String line = inputStream.readLine();
290         if (line.equals(this.startBoundary)) {
291             return true;
292         }
293         else if (line.equals(this.endBoundary)) {
294             return false;
295         }
296         else {
297             throw new MultipartRequestException("Invalid boundary");
298         }
299 
300     }
301 
302     /**
303      * Returns the next element in the iteration.
304      *
305      * @return MultipartElement next element in the iteration.
306      * @throws MultipartRequestException if there is any error procesing multipart
307      * request.
308      * @throws IOException if there is any I/O error.
309      */
310     public MultipartElement next() throws MultipartRequestException, IOException {
311 
312         // Content-Disposition
313         String contentDispositionLine = inputStream.readLine();
314         String contentDisposition = parseContentDisposition(contentDispositionLine);
315         if ((contentDisposition == null) ||
316             (!contentDisposition.equalsIgnoreCase(DEFAULT_CONTENT_DISPOSITION))) {
317             throw new MultipartRequestException("Invalid Content-Disposition");
318         }
319         String name = parseParameter(PARAMETER_NAME, contentDispositionLine);
320         if (name == null) {
321             throw new MultipartRequestException("Invalid Content-Disposition, no parameter name");
322         }
323         String filename = parseParameter(PARAMETER_FILENAME, contentDispositionLine);
324 
325         // Content-Type
326         String contentTypeLine = inputStream.readLine();
327         String contentType = parseContentType(contentTypeLine);
328         String charset = null;
329         if ((contentType == null) || (contentType.length() == 0)) {
330             if (filename == null) {
331                 contentType = CONTENT_TYPE_TEXT_PLAIN;
332             }
333             else {
334                 contentType = CONTENT_TYPE_APPLICATION_OCTET_STREAM;
335             }
336             charset = parseParameter(PARAMETER_CHARSET, contentDispositionLine);
337             if (charset == null) {
338                 charset = this.defaultEncoding;
339             }
340         }
341         else {
342             String line = inputStream.readLine();
343             if (line.length() != 0) {
344                 throw new MultipartRequestException("Invalid Content-Type, no empty line");
345             }
346         }
347 
348         // Data
349         if (filename == null) {
350             String text = readTextValue(charset);
351             return new MultipartElement(name, text);
352         }
353         else {
354             MultipartFile file = null;
355             if (filename.length() != 0) {
356                 file = readFileValue(filename, contentType);
357             }
358             else {
359                 String line = inputStream.readLine();
360                 if (line.length() != 0) {
361                     throw new MultipartRequestException("Invalid null file, no empty line");
362                 }
363             }
364             return new MultipartElement(name, filename, file);
365         }
366 
367     }
368 
369     /**
370      * Parses <code>content-diposition</code> parameter.
371      *
372      * @param line String line to parse.
373      * @return String <code>content-diposition</code> parameter value.
374      */
375     private String parseContentDisposition(String line) {
376 
377         if (!line.toLowerCase().startsWith(CONTENT_DISPOSITION)) {
378             return null;
379         }
380 
381         int beginIndex = line.indexOf(':');
382         if (beginIndex < 0) {
383             return null;
384         }
385 
386         int endIndex = line.indexOf(';');
387         if (endIndex < 0) {
388             return line.substring(beginIndex).trim();
389         }
390 
391         return line.substring(beginIndex + 1, endIndex).trim();
392 
393     }
394 
395     /**
396      * Parses <code>content-type</code> parameter.
397      *
398      * @param line String line to parse.
399      * @return String <code>content-type</code> parameter value.
400      */
401     private String parseContentType(String line) {
402 
403         if (!line.toLowerCase().startsWith(CONTENT_TYPE)) {
404             return null;
405         }
406 
407         int beginIndex = line.indexOf(':');
408         if (beginIndex < 0) {
409             return null;
410         }
411 
412         int endIndex = line.indexOf(';');
413         if (endIndex < 0) {
414             return line.substring(beginIndex).trim();
415         }
416 
417         return line.substring(beginIndex + 1, endIndex).trim();
418 
419     }
420 
421     /**
422      * Parses matching name parameter.
423      *
424      * @param name String name of parameter to parse.
425      * @param line String line to parse.
426      * @return String matching name parameter value or <code>null</code> if not
427      * found.
428      */
429     private String parseParameter(String name, String line) {
430 
431         int index = line.indexOf(name);
432         if (index < 0) {
433             return null;
434         }
435 
436         int beginIndex = line.indexOf('\"', index);
437         if (beginIndex < 0) {
438             return null;
439         }
440 
441         int endIndex = line.indexOf('\"', beginIndex + 1);
442         if (endIndex < 0) {
443             return null;
444         }
445 
446         return line.substring(beginIndex + 1, endIndex);
447 
448     }
449 
450     /**
451      * Reads text element value.
452      *
453      * @param charset String charset encoding.
454      * @return String readed text.
455      * @throws MultipartRequestException if there is any error procesing multipart
456      * request.
457      * @throws IOException if there is any I/O error.
458      */
459     private String readTextValue(String charset) throws MultipartRequestException, IOException {
460         return inputStream.readLine(charset);
461     }
462 
463     /**
464      * Reads file element value.
465      *
466      * @param filename String name of file.
467      * @param contentType String content type of file.
468      * @return MultipartFile readed file.
469      * @throws IOException if there is any I/O error.
470      */
471     private MultipartFile readFileValue(String filename, String contentType)
472     throws IOException {
473 
474         File tmpFile = File.createTempFile(TMP_PREFIX, null, new File(this.tmpDir));
475         BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(tmpFile), DISK_BUFFER_SIZE);
476         byte[] buffer = new byte[this.bufferSize];
477         int count = inputStream.readData(buffer, 0, buffer.length);
478         while (count > 0) {
479             bos.write(buffer, 0, count);
480             count = inputStream.readData(buffer, 0, buffer.length);
481         }
482         bos.close();
483 
484         return new MultipartFile(tmpFile.getAbsolutePath(), filename, contentType);
485 
486     }
487 
488 }