View Javadoc

1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  
28  package org.apache.http.entity.mime;
29  
30  import java.io.ByteArrayOutputStream;
31  import java.io.IOException;
32  import java.io.OutputStream;
33  import java.nio.ByteBuffer;
34  import java.nio.CharBuffer;
35  import java.nio.charset.Charset;
36  import java.util.ArrayList;
37  import java.util.List;
38  
39  import org.apache.http.entity.mime.content.ContentBody;
40  import org.apache.http.util.ByteArrayBuffer;
41  
42  /**
43   * HttpMultipart represents a collection of MIME multipart encoded content bodies. This class is
44   * capable of operating either in the strict (RFC 822, RFC 2045, RFC 2046 compliant) or
45   * the browser compatible modes.
46   *
47   * @since 4.0
48   */
49  public class HttpMultipart {
50  
51      private static ByteArrayBuffer encode(
52              final Charset charset, final String string) {
53          ByteBuffer encoded = charset.encode(CharBuffer.wrap(string));
54          ByteArrayBuffer bab = new ByteArrayBuffer(encoded.remaining());
55          bab.append(encoded.array(), encoded.position(), encoded.remaining());
56          return bab;
57      }
58  
59      private static void writeBytes(
60              final ByteArrayBuffer b, final OutputStream out) throws IOException {
61          out.write(b.buffer(), 0, b.length());
62      }
63  
64      private static void writeBytes(
65              final String s, final Charset charset, final OutputStream out) throws IOException {
66          ByteArrayBuffer b = encode(charset, s);
67          writeBytes(b, out);
68      }
69  
70      private static void writeBytes(
71              final String s, final OutputStream out) throws IOException {
72          ByteArrayBuffer b = encode(MIME.DEFAULT_CHARSET, s);
73          writeBytes(b, out);
74      }
75  
76      private static void writeField(
77              final MinimalField field, final OutputStream out) throws IOException {
78          writeBytes(field.getName(), out);
79          writeBytes(FIELD_SEP, out);
80          writeBytes(field.getBody(), out);
81          writeBytes(CR_LF, out);
82      }
83  
84      private static void writeField(
85              final MinimalField field, final Charset charset, final OutputStream out) throws IOException {
86          writeBytes(field.getName(), charset, out);
87          writeBytes(FIELD_SEP, out);
88          writeBytes(field.getBody(), charset, out);
89          writeBytes(CR_LF, out);
90      }
91  
92      private static final ByteArrayBuffer FIELD_SEP = encode(MIME.DEFAULT_CHARSET, ": ");
93      private static final ByteArrayBuffer CR_LF = encode(MIME.DEFAULT_CHARSET, "\r\n");
94      private static final ByteArrayBuffer TWO_DASHES = encode(MIME.DEFAULT_CHARSET, "--");
95  
96  
97      private final String subType;
98      private final Charset charset;
99      private final String boundary;
100     private final List<FormBodyPart> parts;
101 
102     private final HttpMultipartMode mode;
103 
104     /**
105      * Creates an instance with the specified settings.
106      *
107      * @param subType mime subtype - must not be {@code null}
108      * @param charset the character set to use. May be {@code null}, in which case {@link MIME#DEFAULT_CHARSET} - i.e. US-ASCII - is used.
109      * @param boundary to use  - must not be {@code null}
110      * @param mode the mode to use
111      * @throws IllegalArgumentException if charset is null or boundary is null
112      */
113     public HttpMultipart(final String subType, final Charset charset, final String boundary, HttpMultipartMode mode) {
114         super();
115         if (subType == null) {
116             throw new IllegalArgumentException("Multipart subtype may not be null");
117         }
118         if (boundary == null) {
119             throw new IllegalArgumentException("Multipart boundary may not be null");
120         }
121         this.subType = subType;
122         this.charset = charset != null ? charset : MIME.DEFAULT_CHARSET;
123         this.boundary = boundary;
124         this.parts = new ArrayList<FormBodyPart>();
125         this.mode = mode;
126     }
127 
128     /**
129      * Creates an instance with the specified settings.
130      * Mode is set to {@link HttpMultipartMode#STRICT}
131      *
132      * @param subType mime subtype - must not be {@code null}
133      * @param charset the character set to use. May be {@code null}, in which case {@link MIME#DEFAULT_CHARSET} - i.e. US-ASCII - is used.
134      * @param boundary to use  - must not be {@code null}
135      * @throws IllegalArgumentException if charset is null or boundary is null
136      */
137     public HttpMultipart(final String subType, final Charset charset, final String boundary) {
138         this(subType, charset, boundary, HttpMultipartMode.STRICT);
139     }
140 
141     public HttpMultipart(final String subType, final String boundary) {
142         this(subType, null, boundary);
143     }
144 
145     public String getSubType() {
146         return this.subType;
147     }
148 
149     public Charset getCharset() {
150         return this.charset;
151     }
152 
153     public HttpMultipartMode getMode() {
154         return this.mode;
155     }
156 
157     public List<FormBodyPart> getBodyParts() {
158         return this.parts;
159     }
160 
161     public void addBodyPart(final FormBodyPart part) {
162         if (part == null) {
163             return;
164         }
165         this.parts.add(part);
166     }
167 
168     public String getBoundary() {
169         return this.boundary;
170     }
171 
172     private void doWriteTo(
173         final HttpMultipartMode mode,
174         final OutputStream out,
175         boolean writeContent) throws IOException {
176 
177         ByteArrayBuffer boundary = encode(this.charset, getBoundary());
178         for (FormBodyPart part: this.parts) {
179             writeBytes(TWO_DASHES, out);
180             writeBytes(boundary, out);
181             writeBytes(CR_LF, out);
182 
183             Header header = part.getHeader();
184 
185             switch (mode) {
186             case STRICT:
187                 for (MinimalField field: header) {
188                     writeField(field, out);
189                 }
190                 break;
191             case BROWSER_COMPATIBLE:
192                 // Only write Content-Disposition
193                 // Use content charset
194                 MinimalField cd = part.getHeader().getField(MIME.CONTENT_DISPOSITION);
195                 writeField(cd, this.charset, out);
196                 String filename = part.getBody().getFilename();
197                 if (filename != null) {
198                     MinimalField ct = part.getHeader().getField(MIME.CONTENT_TYPE);
199                     writeField(ct, this.charset, out);
200                 }
201                 break;
202             }
203             writeBytes(CR_LF, out);
204 
205             if (writeContent) {
206                 part.getBody().writeTo(out);
207             }
208             writeBytes(CR_LF, out);
209         }
210         writeBytes(TWO_DASHES, out);
211         writeBytes(boundary, out);
212         writeBytes(TWO_DASHES, out);
213         writeBytes(CR_LF, out);
214     }
215 
216     /**
217      * Writes out the content in the multipart/form encoding. This method
218      * produces slightly different formatting depending on its compatibility
219      * mode.
220      *
221      * @see #getMode()
222      */
223     public void writeTo(final OutputStream out) throws IOException {
224         doWriteTo(this.mode, out, true);
225     }
226 
227     /**
228      * Determines the total length of the multipart content (content length of
229      * individual parts plus that of extra elements required to delimit the parts
230      * from one another). If any of the @{link BodyPart}s contained in this object
231      * is of a streaming entity of unknown length the total length is also unknown.
232      * <p/>
233      * This method buffers only a small amount of data in order to determine the
234      * total length of the entire entity. The content of individual parts is not
235      * buffered.
236      *
237      * @return total length of the multipart entity if known, <code>-1</code>
238      *   otherwise.
239      */
240     public long getTotalLength() {
241         long contentLen = 0;
242         for (FormBodyPart part: this.parts) {
243             ContentBody body = part.getBody();
244             long len = body.getContentLength();
245             if (len >= 0) {
246                 contentLen += len;
247             } else {
248                 return -1;
249             }
250         }
251         ByteArrayOutputStream out = new ByteArrayOutputStream();
252         try {
253             doWriteTo(this.mode, out, false);
254             byte[] extra = out.toByteArray();
255             return contentLen + extra.length;
256         } catch (IOException ex) {
257             // Should never happen
258             return -1;
259         }
260     }
261 
262 }