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.Args;
41  import org.apache.http.util.ByteArrayBuffer;
42  
43  /**
44   * HttpMultipart represents a collection of MIME multipart encoded content bodies. This class is
45   * capable of operating either in the strict (RFC 822, RFC 2045, RFC 2046 compliant) or
46   * the browser compatible modes.
47   *
48   * @since 4.0
49   */
50  public class HttpMultipart {
51  
52      private static ByteArrayBuffer encode(
53              final Charset charset, final String string) {
54          final ByteBuffer encoded = charset.encode(CharBuffer.wrap(string));
55          final ByteArrayBuffer bab = new ByteArrayBuffer(encoded.remaining());
56          bab.append(encoded.array(), encoded.position(), encoded.remaining());
57          return bab;
58      }
59  
60      private static void writeBytes(
61              final ByteArrayBuffer b, final OutputStream out) throws IOException {
62          out.write(b.buffer(), 0, b.length());
63      }
64  
65      private static void writeBytes(
66              final String s, final Charset charset, final OutputStream out) throws IOException {
67          final ByteArrayBuffer b = encode(charset, s);
68          writeBytes(b, out);
69      }
70  
71      private static void writeBytes(
72              final String s, final OutputStream out) throws IOException {
73          final ByteArrayBuffer b = encode(MIME.DEFAULT_CHARSET, s);
74          writeBytes(b, out);
75      }
76  
77      private static void writeField(
78              final MinimalField field, final OutputStream out) throws IOException {
79          writeBytes(field.getName(), out);
80          writeBytes(FIELD_SEP, out);
81          writeBytes(field.getBody(), out);
82          writeBytes(CR_LF, out);
83      }
84  
85      private static void writeField(
86              final MinimalField field, final Charset charset, final OutputStream out) throws IOException {
87          writeBytes(field.getName(), charset, out);
88          writeBytes(FIELD_SEP, out);
89          writeBytes(field.getBody(), charset, out);
90          writeBytes(CR_LF, out);
91      }
92  
93      private static final ByteArrayBuffer FIELD_SEP = encode(MIME.DEFAULT_CHARSET, ": ");
94      private static final ByteArrayBuffer CR_LF = encode(MIME.DEFAULT_CHARSET, "\r\n");
95      private static final ByteArrayBuffer TWO_DASHES = encode(MIME.DEFAULT_CHARSET, "--");
96  
97  
98      private final String subType;
99      private final Charset charset;
100     private final String boundary;
101     private final List<FormBodyPart> parts;
102 
103     private final HttpMultipartMode mode;
104 
105     /**
106      * Creates an instance with the specified settings.
107      *
108      * @param subType mime subtype - must not be {@code null}
109      * @param charset the character set to use. May be {@code null}, in which case {@link MIME#DEFAULT_CHARSET} - i.e. US-ASCII - is used.
110      * @param boundary to use  - must not be {@code null}
111      * @param mode the mode to use
112      * @throws IllegalArgumentException if charset is null or boundary is null
113      */
114     public HttpMultipart(final String subType, final Charset charset, final String boundary, final HttpMultipartMode mode) {
115         super();
116         Args.notNull(subType, "Multipart subtype");
117         Args.notNull(boundary, "Multipart boundary");
118         this.subType = subType;
119         this.charset = charset != null ? charset : MIME.DEFAULT_CHARSET;
120         this.boundary = boundary;
121         this.parts = new ArrayList<FormBodyPart>();
122         this.mode = mode;
123     }
124 
125     /**
126      * Creates an instance with the specified settings.
127      * Mode is set to {@link HttpMultipartMode#STRICT}
128      *
129      * @param subType mime subtype - must not be {@code null}
130      * @param charset the character set to use. May be {@code null}, in which case {@link MIME#DEFAULT_CHARSET} - i.e. US-ASCII - is used.
131      * @param boundary to use  - must not be {@code null}
132      * @throws IllegalArgumentException if charset is null or boundary is null
133      */
134     public HttpMultipart(final String subType, final Charset charset, final String boundary) {
135         this(subType, charset, boundary, HttpMultipartMode.STRICT);
136     }
137 
138     public HttpMultipart(final String subType, final String boundary) {
139         this(subType, null, boundary);
140     }
141 
142     public String getSubType() {
143         return this.subType;
144     }
145 
146     public Charset getCharset() {
147         return this.charset;
148     }
149 
150     public HttpMultipartMode getMode() {
151         return this.mode;
152     }
153 
154     public List<FormBodyPart> getBodyParts() {
155         return this.parts;
156     }
157 
158     public void addBodyPart(final FormBodyPart part) {
159         if (part == null) {
160             return;
161         }
162         this.parts.add(part);
163     }
164 
165     public String getBoundary() {
166         return this.boundary;
167     }
168 
169     private void doWriteTo(
170         final HttpMultipartMode mode,
171         final OutputStream out,
172         final boolean writeContent) throws IOException {
173 
174         final ByteArrayBuffer boundary = encode(this.charset, getBoundary());
175         for (final FormBodyPart part: this.parts) {
176             writeBytes(TWO_DASHES, out);
177             writeBytes(boundary, out);
178             writeBytes(CR_LF, out);
179 
180             final Header header = part.getHeader();
181 
182             switch (mode) {
183             case STRICT:
184                 for (final MinimalField field: header) {
185                     writeField(field, out);
186                 }
187                 break;
188             case BROWSER_COMPATIBLE:
189                 // Only write Content-Disposition
190                 // Use content charset
191                 final MinimalField cd = part.getHeader().getField(MIME.CONTENT_DISPOSITION);
192                 writeField(cd, this.charset, out);
193                 final String filename = part.getBody().getFilename();
194                 if (filename != null) {
195                     final MinimalField ct = part.getHeader().getField(MIME.CONTENT_TYPE);
196                     writeField(ct, this.charset, out);
197                 }
198                 break;
199             }
200             writeBytes(CR_LF, out);
201 
202             if (writeContent) {
203                 part.getBody().writeTo(out);
204             }
205             writeBytes(CR_LF, out);
206         }
207         writeBytes(TWO_DASHES, out);
208         writeBytes(boundary, out);
209         writeBytes(TWO_DASHES, out);
210         writeBytes(CR_LF, out);
211     }
212 
213     /**
214      * Writes out the content in the multipart/form encoding. This method
215      * produces slightly different formatting depending on its compatibility
216      * mode.
217      *
218      * @see #getMode()
219      */
220     public void writeTo(final OutputStream out) throws IOException {
221         doWriteTo(this.mode, out, true);
222     }
223 
224     /**
225      * Determines the total length of the multipart content (content length of
226      * individual parts plus that of extra elements required to delimit the parts
227      * from one another). If any of the @{link BodyPart}s contained in this object
228      * is of a streaming entity of unknown length the total length is also unknown.
229      * <p/>
230      * This method buffers only a small amount of data in order to determine the
231      * total length of the entire entity. The content of individual parts is not
232      * buffered.
233      *
234      * @return total length of the multipart entity if known, <code>-1</code>
235      *   otherwise.
236      */
237     public long getTotalLength() {
238         long contentLen = 0;
239         for (final FormBodyPart part: this.parts) {
240             final ContentBody body = part.getBody();
241             final long len = body.getContentLength();
242             if (len >= 0) {
243                 contentLen += len;
244             } else {
245                 return -1;
246             }
247         }
248         final ByteArrayOutputStream out = new ByteArrayOutputStream();
249         try {
250             doWriteTo(this.mode, out, false);
251             final byte[] extra = out.toByteArray();
252             return contentLen + extra.length;
253         } catch (final IOException ex) {
254             // Should never happen
255             return -1;
256         }
257     }
258 
259 }