001: /*
002: * $Header: /home/cvs/jakarta-commons/fileupload/src/java/org/apache/commons/fileupload/MultipartStream.java,v 1.12 2003/06/01 00:18:13 martinc Exp $
003: * $Revision: 1.12 $
004: * $Date: 2003/06/01 00:18:13 $
005: *
006: * ====================================================================
007: *
008: * The Apache Software License, Version 1.1
009: *
010: * Copyright (c) 2001-2003 The Apache Software Foundation. All rights
011: * reserved.
012: *
013: * Redistribution and use in source and binary forms, with or without
014: * modification, are permitted provided that the following conditions
015: * are met:
016: *
017: * 1. Redistributions of source code must retain the above copyright
018: * notice, this list of conditions and the following disclaimer.
019: *
020: * 2. Redistributions in binary form must reproduce the above copyright
021: * notice, this list of conditions and the following disclaimer in
022: * the documentation and/or other materials provided with the
023: * distribution.
024: *
025: * 3. The end-user documentation included with the redistribution, if
026: * any, must include the following acknowlegement:
027: * "This product includes software developed by the
028: * Apache Software Foundation (http://www.apache.org/)."
029: * Alternately, this acknowlegement may appear in the software itself,
030: * if and wherever such third-party acknowlegements normally appear.
031: *
032: * 4. The names "The Jakarta Project", "Commons", and "Apache Software
033: * Foundation" must not be used to endorse or promote products derived
034: * from this software without prior written permission. For written
035: * permission, please contact apache@apache.org.
036: *
037: * 5. Products derived from this software may not be called "Apache"
038: * nor may "Apache" appear in their names without prior written
039: * permission of the Apache Group.
040: *
041: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
042: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
043: * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
044: * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
045: * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
046: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
047: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
048: * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
049: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
050: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
051: * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
052: * SUCH DAMAGE.
053: * ====================================================================
054: *
055: * This software consists of voluntary contributions made by many
056: * individuals on behalf of the Apache Software Foundation. For more
057: * information on the Apache Software Foundation, please see
058: * <http://www.apache.org/>.
059: *
060: */
061:
062: package de.ug2t.extTools.httpFileUpLoad;
063:
064: import java.io.*;
065:
066: /**
067: * <p>
068: * Low level API for processing file uploads.
069: *
070: * <p>
071: * This class can be used to process data streams conforming to MIME 'multipart'
072: * format as defined in <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>.
073: * Arbitrarily large amounts of data in the stream can be processed under
074: * constant memory usage.
075: *
076: * <p>
077: * The format of the stream is defined in the following way:<br>
078: *
079: * <code>
080: * multipart-body := preamble 1*encapsulation close-delimiter epilogue<br>
081: * encapsulation := delimiter body CRLF<br>
082: * delimiter := "--" boundary CRLF<br>
083: * close-delimiter := "--" boudary "--"<br>
084: * preamble := <ignore><br>
085: * epilogue := <ignore><br>
086: * body := header-part CRLF body-part<br>
087: * header-part := 1*header CRLF<br>
088: * header := header-name ":" header-value<br>
089: * header-name := <printable ascii characters except ":"><br>
090: * header-value := <any ascii characters except CR & LF><br>
091: * body-data := <arbitrary data><br>
092: * </code>
093: *
094: * <p>
095: * Note that body-data can contain another mulipart entity. There is limited
096: * support for single pass processing of such nested streams. The nested stream
097: * is <strong>required</strong> to have a boundary token of the same length as
098: * the parent stream (see {@link #setBoundary(byte[])}).
099: *
100: * <p>
101: * Here is an exaple of usage of this class.<br>
102: *
103: * <pre>
104: * try {
105: * MultipartStream multipartStream = new MultipartStream(input,
106: * boundary);
107: * boolean nextPart = malitPartStream.skipPreamble();
108: * OutputStream output;
109: * while(nextPart) {
110: * header = chunks.readHeader();
111: * // process headers
112: * // create some output stream
113: * multipartStream.readBodyPart(output);
114: * nextPart = multipartStream.readBoundary();
115: * }
116: * } catch(MultipartStream.MalformedStreamException e) {
117: * // the stream failed to follow required syntax
118: * } catch(IOException) {
119: * // a read or write error occurred
120: * }
121: *
122: * </pre>
123: *
124: * @author <a href="mailto:Rafal.Krzewski@e-point.pl">Rafal Krzewski</a>
125: * @author <a href="mailto:martinc@apache.org">Martin Cooper</a>
126: * @author Sean C. Sullivan
127: *
128: * @version $Id: MultipartStream.java,v 1.12 2003/06/01 00:18:13 martinc Exp $
129: */
130: public class MultipartStream {
131:
132: // ----------------------------------------------------- Manifest constants
133:
134: /**
135: * The maximum length of <code>header-part</code> that will be processed (10
136: * kilobytes = 10240 bytes.).
137: */
138: public static final int HEADER_PART_SIZE_MAX = 10240;
139:
140: /**
141: * The default length of the buffer used for processing a request.
142: */
143: protected static final int DEFAULT_BUFSIZE = 4096;
144:
145: /**
146: * A byte sequence that marks the end of <code>header-part</code> (<code>CRLFCRLF</code>).
147: */
148: protected static final byte[] HEADER_SEPARATOR = { 0x0D, 0x0A,
149: 0x0D, 0x0A };
150:
151: /**
152: * A byte sequence that that follows a delimiter that will be followed by an
153: * encapsulation (<code>CRLF</code>).
154: */
155: protected static final byte[] FIELD_SEPARATOR = { 0x0D, 0x0A };
156:
157: /**
158: * A byte sequence that that follows a delimiter of the last encapsulation in
159: * the stream (<code>--</code>).
160: */
161: protected static final byte[] STREAM_TERMINATOR = { 0x2D, 0x2D };
162:
163: // ----------------------------------------------------------- Data members
164:
165: /**
166: * The input stream from which data is read.
167: */
168: private InputStream input;
169:
170: /**
171: * The length of the boundary token plus the leading <code>CRLF--</code>.
172: */
173: private int boundaryLength;
174:
175: /**
176: * The amount of data, in bytes, that must be kept in the buffer in order to
177: * detect delimiters reliably.
178: */
179: private int keepRegion;
180:
181: /**
182: * The byte sequence that partitions the stream.
183: */
184: private byte[] boundary;
185:
186: /**
187: * The length of the buffer used for processing the request.
188: */
189: private int bufSize;
190:
191: /**
192: * The buffer used for processing the request.
193: */
194: private byte[] buffer;
195:
196: /**
197: * The index of first valid character in the buffer. <br>
198: * 0 <= head < bufSize
199: */
200: private int head;
201:
202: /**
203: * The index of last valid characer in the buffer + 1. <br>
204: * 0 <= tail <= bufSize
205: */
206: private int tail;
207:
208: /**
209: * The content encoding to use when reading headers.
210: */
211: private String headerEncoding;
212:
213: // ----------------------------------------------------------- Constructors
214:
215: /**
216: * Default constructor.
217: *
218: * @see #MultipartStream(InputStream, byte[], int)
219: * @see #MultipartStream(InputStream, byte[])
220: *
221: */
222: public MultipartStream() {
223: }
224:
225: /**
226: * <p>
227: * Constructs a <code>MultipartStream</code> with a custom size buffer.
228: *
229: * <p>
230: * Note that the buffer must be at least big enough to contain the boundary
231: * string, plus 4 characters for CR/LF and double dash, plus at least one byte
232: * of data. Too small a buffer size setting will degrade performance.
233: *
234: * @param input
235: * The <code>InputStream</code> to serve as a data source.
236: * @param boundary
237: * The token used for dividing the stream into
238: * <code>encapsulations</code>.
239: * @param bufSize
240: * The size of the buffer to be used, in bytes.
241: *
242: *
243: * @see #MultipartStream()
244: * @see #MultipartStream(InputStream, byte[])
245: *
246: */
247: public MultipartStream(InputStream input, byte[] boundary,
248: int bufSize) {
249: this .input = input;
250: this .bufSize = bufSize;
251: this .buffer = new byte[bufSize];
252:
253: // We prepend CR/LF to the boundary to chop trailng CR/LF from
254: // body-data tokens.
255: this .boundary = new byte[boundary.length + 4];
256: this .boundaryLength = boundary.length + 4;
257: this .keepRegion = boundary.length + 3;
258: this .boundary[0] = 0x0D;
259: this .boundary[1] = 0x0A;
260: this .boundary[2] = 0x2D;
261: this .boundary[3] = 0x2D;
262: System
263: .arraycopy(boundary, 0, this .boundary, 4,
264: boundary.length);
265:
266: head = 0;
267: tail = 0;
268: }
269:
270: /**
271: * <p>
272: * Constructs a <code>MultipartStream</code> with a default size buffer.
273: *
274: * @param input
275: * The <code>InputStream</code> to serve as a data source.
276: * @param boundary
277: * The token used for dividing the stream into
278: * <code>encapsulations</code>.
279: *
280: * @exception IOException
281: * when an error occurs.
282: *
283: * @see #MultipartStream()
284: * @see #MultipartStream(InputStream, byte[], int)
285: *
286: */
287: public MultipartStream(InputStream input, byte[] boundary)
288: throws IOException {
289: this (input, boundary, DEFAULT_BUFSIZE);
290: }
291:
292: // --------------------------------------------------------- Public methods
293:
294: /**
295: * Retrieves the character encoding used when reading the headers of an
296: * individual part. When not specified, or <code>null</code>, the platform
297: * default encoding is used.
298: *
299: *
300: * @return The encoding used to read part headers.
301: */
302: public String getHeaderEncoding() {
303: return headerEncoding;
304: }
305:
306: /**
307: * Specifies the character encoding to be used when reading the headers of
308: * individual parts. When not specified, or <code>null</code>, the platform
309: * default encoding is used.
310: *
311: * @param encoding
312: * The encoding used to read part headers.
313: */
314: public void setHeaderEncoding(String encoding) {
315: headerEncoding = encoding;
316: }
317:
318: /**
319: * Reads a byte from the <code>buffer</code>, and refills it as necessary.
320: *
321: * @return The next byte from the input stream.
322: *
323: * @exception IOException
324: * if there is no more data available.
325: */
326: public byte readByte() throws IOException {
327: // Buffer depleted ?
328: if (head == tail) {
329: head = 0;
330: // Refill.
331: tail = input.read(buffer, head, bufSize);
332: if (tail == -1) {
333: // No more data available.
334: throw new IOException("No more data is available");
335: }
336: }
337: return buffer[head++];
338: }
339:
340: /**
341: * Skips a <code>boundary</code> token, and checks whether more
342: * <code>encapsulations</code> are contained in the stream.
343: *
344: * @return <code>true</code> if there are more encapsulations in this
345: * stream; <code>false</code> otherwise.
346: *
347: * @exception MalformedStreamException
348: * if the stream ends unexpecetedly or fails to follow required
349: * syntax.
350: */
351: public boolean readBoundary() throws MalformedStreamException {
352: byte[] marker = new byte[2];
353: boolean nextChunk = false;
354:
355: head += boundaryLength;
356: try {
357: marker[0] = readByte();
358: marker[1] = readByte();
359: if (arrayequals(marker, STREAM_TERMINATOR, 2)) {
360: nextChunk = false;
361: } else if (arrayequals(marker, FIELD_SEPARATOR, 2)) {
362: nextChunk = true;
363: } else {
364: throw new MalformedStreamException(
365: "Unexpected characters follow a boundary");
366: }
367: } catch (IOException e) {
368: throw new MalformedStreamException(
369: "Stream ended unexpectedly");
370: }
371: return nextChunk;
372: }
373:
374: /**
375: * <p>
376: * Changes the boundary token used for partitioning the stream.
377: *
378: * <p>
379: * This method allows single pass processing of nested multipart streams.
380: *
381: * <p>
382: * The boundary token of the nested stream is <code>required</code> to be of
383: * the same length as the boundary token in parent stream.
384: *
385: * <p>
386: * Restoring the parent stream boundary token after processing of a nested
387: * stream is left to the application.
388: *
389: * @param boundary
390: * The boundary to be used for parsing of the nested stream.
391: *
392: * @exception IllegalBoundaryException
393: * if the <code>boundary</code> has a different length than the
394: * one being currently parsed.
395: */
396: public void setBoundary(byte[] boundary)
397: throws IllegalBoundaryException {
398: if (boundary.length != boundaryLength - 4) {
399: throw new IllegalBoundaryException(
400: "The length of a boundary token can not be changed");
401: }
402: System
403: .arraycopy(boundary, 0, this .boundary, 4,
404: boundary.length);
405: }
406:
407: /**
408: * <p>
409: * Reads the <code>header-part</code> of the current
410: * <code>encapsulation</code>.
411: *
412: * <p>
413: * Headers are returned verbatim to the input stream, including the trailing
414: * <code>CRLF</code> marker. Parsing is left to the application.
415: *
416: * <p>
417: * <strong>TODO</strong> allow limiting maximum header size to protect
418: * against abuse.
419: *
420: * @return The <code>header-part</code> of the current encapsulation.
421: *
422: * @exception MalformedStreamException
423: * if the stream ends unexpecetedly.
424: */
425: public String readHeaders() throws MalformedStreamException {
426: int i = 0;
427: byte b[] = new byte[1];
428: // to support multi-byte characters
429: ByteArrayOutputStream baos = new ByteArrayOutputStream();
430: int sizeMax = HEADER_PART_SIZE_MAX;
431: int size = 0;
432: while (i < 4) {
433: try {
434: b[0] = readByte();
435: } catch (IOException e) {
436: throw new MalformedStreamException(
437: "Stream ended unexpectedly");
438: }
439: size++;
440: if (b[0] == HEADER_SEPARATOR[i]) {
441: i++;
442: } else {
443: i = 0;
444: }
445: if (size <= sizeMax) {
446: baos.write(b[0]);
447: }
448: }
449:
450: String headers = null;
451: if (headerEncoding != null) {
452: try {
453: headers = baos.toString(headerEncoding);
454: } catch (UnsupportedEncodingException e) {
455: // Fall back to platform default if specified encoding is not
456: // supported.
457: headers = baos.toString();
458: }
459: } else {
460: headers = baos.toString();
461: }
462:
463: return headers;
464: }
465:
466: /**
467: * <p>
468: * Reads <code>body-data</code> from the current <code>encapsulation</code>
469: * and writes its contents into the output <code>Stream</code>.
470: *
471: * <p>
472: * Arbitrary large amounts of data can be processed by this method using a
473: * constant size buffer. (see {@link #MultipartStream(InputStream,byte[],int)
474: * constructor}).
475: *
476: * @param output
477: * The <code>Stream</code> to write data into.
478: *
479: * @return the amount of data written.
480: *
481: * @exception MalformedStreamException
482: * if the stream ends unexpectedly.
483: * @exception IOException
484: * if an i/o error occurs.
485: */
486: public int readBodyData(OutputStream output)
487: throws MalformedStreamException, IOException {
488: boolean done = false;
489: int pad;
490: int pos;
491: int bytesRead;
492: int total = 0;
493: while (!done) {
494: // Is boundary token present somewere in the buffer?
495: pos = findSeparator();
496: if (pos != -1) {
497: // Write the rest of the data before the boundary.
498: output.write(buffer, head, pos - head);
499: total += pos - head;
500: head = pos;
501: done = true;
502: } else {
503: // Determine how much data should be kept in the
504: // buffer.
505: if (tail - head > keepRegion) {
506: pad = keepRegion;
507: } else {
508: pad = tail - head;
509: }
510: // Write out the data belonging to the body-data.
511: output.write(buffer, head, tail - head - pad);
512:
513: // Move the data to the beging of the buffer.
514: total += tail - head - pad;
515: System.arraycopy(buffer, tail - pad, buffer, 0, pad);
516:
517: // Refill buffer with new data.
518: head = 0;
519: bytesRead = input.read(buffer, pad, bufSize - pad);
520:
521: // [pprrrrrrr]
522: if (bytesRead != -1) {
523: tail = pad + bytesRead;
524: } else {
525: // The last pad amount is left in the buffer.
526: // Boundary can't be in there so write out the
527: // data you have and signal an error condition.
528: output.write(buffer, 0, pad);
529: output.flush();
530: total += pad;
531: throw new MalformedStreamException(
532: "Stream ended unexpectedly");
533: }
534: }
535: }
536: output.flush();
537: return total;
538: }
539:
540: /**
541: * <p>
542: * Reads <code>body-data</code> from the current <code>encapsulation</code>
543: * and discards it.
544: *
545: * <p>
546: * Use this method to skip encapsulations you don't need or don't understand.
547: *
548: * @return The amount of data discarded.
549: *
550: * @exception MalformedStreamException
551: * if the stream ends unexpectedly.
552: * @exception IOException
553: * if an i/o error occurs.
554: */
555: public int discardBodyData() throws MalformedStreamException,
556: IOException {
557: boolean done = false;
558: int pad;
559: int pos;
560: int bytesRead;
561: int total = 0;
562: while (!done) {
563: // Is boundary token present somewere in the buffer?
564: pos = findSeparator();
565: if (pos != -1) {
566: // Write the rest of the data before the boundary.
567: total += pos - head;
568: head = pos;
569: done = true;
570: } else {
571: // Determine how much data should be kept in the
572: // buffer.
573: if (tail - head > keepRegion) {
574: pad = keepRegion;
575: } else {
576: pad = tail - head;
577: }
578: total += tail - head - pad;
579:
580: // Move the data to the beging of the buffer.
581: System.arraycopy(buffer, tail - pad, buffer, 0, pad);
582:
583: // Refill buffer with new data.
584: head = 0;
585: bytesRead = input.read(buffer, pad, bufSize - pad);
586:
587: // [pprrrrrrr]
588: if (bytesRead != -1) {
589: tail = pad + bytesRead;
590: } else {
591: // The last pad amount is left in the buffer.
592: // Boundary can't be in there so signal an error
593: // condition.
594: total += pad;
595: throw new MalformedStreamException(
596: "Stream ended unexpectedly");
597: }
598: }
599: }
600: return total;
601: }
602:
603: /**
604: * Finds the beginning of the first <code>encapsulation</code>.
605: *
606: * @return <code>true</code> if an <code>encapsulation</code> was found in
607: * the stream.
608: *
609: * @exception IOException
610: * if an i/o error occurs.
611: */
612: public boolean skipPreamble() throws IOException {
613: // First delimiter may be not preceeded with a CRLF.
614: System.arraycopy(boundary, 2, boundary, 0, boundary.length - 2);
615: boundaryLength = boundary.length - 2;
616: try {
617: // Discard all data up to the delimiter.
618: discardBodyData();
619:
620: // Read boundary - if succeded, the stream contains an
621: // encapsulation.
622: return readBoundary();
623: } catch (MalformedStreamException e) {
624: return false;
625: } finally {
626: // Restore delimiter.
627: System.arraycopy(boundary, 0, boundary, 2,
628: boundary.length - 2);
629: boundaryLength = boundary.length;
630: boundary[0] = 0x0D;
631: boundary[1] = 0x0A;
632: }
633: }
634:
635: /**
636: * Compares <code>count</code> first bytes in the arrays <code>a</code>
637: * and <code>b</code>.
638: *
639: * @param a
640: * The first array to compare.
641: * @param b
642: * The second array to compare.
643: * @param count
644: * How many bytes should be compared.
645: *
646: * @return <code>true</code> if <code>count</code> first bytes in arrays
647: * <code>a</code> and <code>b</code> are equal.
648: */
649: public static boolean arrayequals(byte[] a, byte[] b, int count) {
650: for (int i = 0; i < count; i++) {
651: if (a[i] != b[i]) {
652: return false;
653: }
654: }
655: return true;
656: }
657:
658: /**
659: * Searches for a byte of specified value in the <code>buffer</code>,
660: * starting at the specified <code>position</code>.
661: *
662: * @param value
663: * The value to find.
664: * @param pos
665: * The starting position for searching.
666: *
667: * @return The position of byte found, counting from beginning of the
668: * <code>buffer</code>, or <code>-1</code> if not found.
669: */
670: protected int findByte(byte value, int pos) {
671: for (int i = pos; i < tail; i++) {
672: if (buffer[i] == value) {
673: return i;
674: }
675: }
676:
677: return -1;
678: }
679:
680: /**
681: * Searches for the <code>boundary</code> in the <code>buffer</code>
682: * region delimited by <code>head</code> and <code>tail</code>.
683: *
684: * @return The position of the boundary found, counting from the beginning of
685: * the <code>buffer</code>, or <code>-1</code> if not found.
686: */
687: protected int findSeparator() {
688: int first;
689: int match = 0;
690: int maxpos = tail - boundaryLength;
691: for (first = head; (first <= maxpos)
692: && (match != boundaryLength); first++) {
693: first = findByte(boundary[0], first);
694: if (first == -1 || (first > maxpos)) {
695: return -1;
696: }
697: for (match = 1; match < boundaryLength; match++) {
698: if (buffer[first + match] != boundary[match]) {
699: break;
700: }
701: }
702: }
703: if (match == boundaryLength) {
704: return first - 1;
705: }
706: return -1;
707: }
708:
709: /**
710: * Returns a string representation of this object.
711: *
712: * @return The string representation of this object.
713: */
714: public String toString() {
715: StringBuffer sbTemp = new StringBuffer();
716: sbTemp.append("boundary='");
717: sbTemp.append(String.valueOf(boundary));
718: sbTemp.append("'\nbufSize=");
719: sbTemp.append(bufSize);
720: return sbTemp.toString();
721: }
722:
723: /**
724: * Thrown to indicate that the input stream fails to follow the required
725: * syntax.
726: */
727: public class MalformedStreamException extends IOException {
728: /**
729: * Constructs a <code>MalformedStreamException</code> with no detail
730: * message.
731: */
732: public MalformedStreamException() {
733: super ();
734: }
735:
736: /**
737: * Constructs an <code>MalformedStreamException</code> with the specified
738: * detail message.
739: *
740: * @param message
741: * The detail message.
742: */
743: public MalformedStreamException(String message) {
744: super (message);
745: }
746: }
747:
748: /**
749: * Thrown upon attempt of setting an invalid boundary token.
750: */
751: public class IllegalBoundaryException extends IOException {
752: /**
753: * Constructs an <code>IllegalBoundaryException</code> with no detail
754: * message.
755: */
756: public IllegalBoundaryException() {
757: super ();
758: }
759:
760: /**
761: * Constructs an <code>IllegalBoundaryException</code> with the specified
762: * detail message.
763: *
764: * @param message
765: * The detail message.
766: */
767: public IllegalBoundaryException(String message) {
768: super (message);
769: }
770: }
771:
772: // ------------------------------------------------------ Debugging methods
773:
774: // These are the methods that were used to debug this stuff.
775: /*
776: * // Dump data. protected void dump() { System.out.println("01234567890");
777: * byte[] temp = new byte[buffer.length]; for(int i=0; i<buffer.length; i++) {
778: * if (buffer[i] == 0x0D || buffer[i] == 0x0A) { temp[i] = 0x21; } else {
779: * temp[i] = buffer[i]; } } System.out.println(new String(temp)); int i; for
780: * (i=0; i<head; i++) System.out.print(" "); System.out.println("h"); for
781: * (i=0; i<tail; i++) System.out.print(" "); System.out.println("t");
782: * System.out.flush(); } // Main routine, for testing purposes only. // //
783: * @param args A String[] with the command line arguments. // @exception
784: * Exception, a generic exception. public static void main( String[] args )
785: * throws Exception { File boundaryFile = new File("boundary.dat"); int
786: * boundarySize = (int)boundaryFile.length(); byte[] boundary = new
787: * byte[boundarySize]; FileInputStream input = new
788: * FileInputStream(boundaryFile); input.read(boundary,0,boundarySize);
789: *
790: * input = new FileInputStream("multipart.dat"); MultipartStream chunks = new
791: * MultipartStream(input, boundary);
792: *
793: * int i = 0; String header; OutputStream output; boolean nextChunk =
794: * chunks.skipPreamble(); while (nextChunk) { header = chunks.readHeaders();
795: * System.out.println("!"+header+"!"); System.out.println("wrote
796: * part"+i+".dat"); output = new FileOutputStream("part"+(i++)+".dat");
797: * chunks.readBodyData(output); nextChunk = chunks.readBoundary(); } }
798: *
799: */
800: }
|