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.hc.client5.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.nio.charset.StandardCharsets;
37  import java.util.List;
38  
39  import org.apache.hc.core5.util.Args;
40  import org.apache.hc.core5.util.ByteArrayBuffer;
41  
42  /**
43   * HttpMultipart represents a collection of MIME multipart encoded content bodies.
44   *
45   * @since 4.3
46   */
47  abstract class AbstractMultipartFormat {
48  
49      /**
50       * The preamble to be included before the multipart content.
51       */
52      private String preamble;
53  
54      /**
55       * The epilogue to be included after the multipart content.
56       */
57      private String epilogue;
58  
59      static ByteArrayBuffer encode(
60              final Charset charset, final CharSequence string) {
61          final ByteBuffer encoded = charset.encode(CharBuffer.wrap(string));
62          final ByteArrayBuffer bab = new ByteArrayBuffer(encoded.remaining());
63          bab.append(encoded.array(), encoded.arrayOffset() + encoded.position(), encoded.remaining());
64          return bab;
65      }
66  
67      static void writeBytes(
68              final ByteArrayBuffer b, final OutputStream out) throws IOException {
69          out.write(b.array(), 0, b.length());
70      }
71  
72      static void writeBytes(
73              final CharSequence s, final Charset charset, final OutputStream out) throws IOException {
74          final ByteArrayBuffer b = encode(charset, s);
75          writeBytes(b, out);
76      }
77  
78      static void writeBytes(
79              final CharSequence s, final OutputStream out) throws IOException {
80          final ByteArrayBuffer b = encode(StandardCharsets.ISO_8859_1, s);
81          writeBytes(b, out);
82      }
83  
84      static boolean isLineBreak(final char ch) {
85          return ch == '\r' || ch == '\n' || ch == '\f' || ch == 11;
86      }
87  
88      static CharSequence stripLineBreaks(final CharSequence s) {
89          if (s == null) {
90              return null;
91          }
92          boolean requiresRewrite = false;
93          int n = 0;
94          for (; n < s.length(); n++) {
95              final char ch = s.charAt(n);
96              if (isLineBreak(ch)) {
97                  requiresRewrite = true;
98                  break;
99              }
100         }
101         if (!requiresRewrite) {
102             return s;
103         }
104         final StringBuilder buf = new StringBuilder();
105         buf.append(s, 0, n);
106         for (; n < s.length(); n++) {
107             final char ch = s.charAt(n);
108             if (isLineBreak(ch)) {
109                 buf.append(' ');
110             } else {
111                 buf.append(ch);
112             }
113         }
114         return buf.toString();
115     }
116 
117     static void writeField(
118             final MimeField field, final OutputStream out) throws IOException {
119         writeBytes(stripLineBreaks(field.getName()), out);
120         writeBytes(FIELD_SEP, out);
121         writeBytes(stripLineBreaks(field.getBody()), out);
122         writeBytes(CR_LF, out);
123     }
124 
125     static void writeField(
126             final MimeField field, final Charset charset, final OutputStream out) throws IOException {
127         writeBytes(stripLineBreaks(field.getName()), charset, out);
128         writeBytes(FIELD_SEP, out);
129         writeBytes(stripLineBreaks(field.getBody()), charset, out);
130         writeBytes(CR_LF, out);
131     }
132 
133     static final ByteArrayBuffer FIELD_SEP = encode(StandardCharsets.ISO_8859_1, ": ");
134     static final ByteArrayBuffer CR_LF = encode(StandardCharsets.ISO_8859_1, "\r\n");
135     static final ByteArrayBuffer TWO_HYPHENS = encode(StandardCharsets.ISO_8859_1, "--");
136 
137     final Charset charset;
138     final String boundary;
139 
140     /**
141      * Creates an instance with the specified settings.
142      *
143      * @param charset  the character set to use. May be {@code null}, in which case {@link StandardCharsets#ISO_8859_1} is used.
144      * @param boundary to use  - must not be {@code null}
145      * @throws IllegalArgumentException if charset is null or boundary is null
146      */
147     public AbstractMultipartFormat(final Charset charset, final String boundary) {
148         super();
149         Args.notNull(boundary, "Multipart boundary");
150         this.charset = charset != null ? charset : StandardCharsets.ISO_8859_1;
151         this.boundary = boundary;
152     }
153 
154     /*  */
155 
156     /**
157      * Constructs a new instance of {@code AbstractMultipartFormat} with the given charset, boundary, preamble, and epilogue.
158      *
159      * @param charset  the charset to use.
160      * @param boundary the boundary string to use.
161      * @param preamble the preamble string to use. Can be {@code null}.
162      * @param epilogue the epilogue string to use. Can be {@code null}.
163      * @throws IllegalArgumentException if the boundary string is {@code null}.
164      */
165     public AbstractMultipartFormat(final Charset charset, final String boundary, final String preamble, final String epilogue) {
166         super();
167         Args.notNull(boundary, "Multipart boundary");
168         this.charset = charset != null ? charset : StandardCharsets.ISO_8859_1;
169         this.boundary = boundary;
170         this.preamble = preamble;
171         this.epilogue = epilogue;
172     }
173 
174     public AbstractMultipartFormat(final String boundary) {
175         this(null, boundary);
176     }
177 
178     public abstract List<MultipartPart> getParts();
179 
180     /**
181      * Writes the multipart message to the specified output stream.
182      * <p>
183      * If {@code writeContent} is {@code true}, the content of each part will also be written.
184      *
185      * <p>If {@code preamble} is not {@code null}, it will be written before the first boundary.
186      * If {@code epilogue} is not {@code null}, it will be written after the last boundary.
187      *
188      * @param out          the output stream to write the message to.
189      * @param writeContent whether to write the content of each part.
190      * @throws IOException if an I/O error occurs.
191      */
192     void doWriteTo(
193             final OutputStream out,
194             final boolean writeContent) throws IOException {
195 
196         final ByteArrayBuffer boundaryEncoded = encode(this.charset, this.boundary);
197         if (this.preamble != null) {
198             writeBytes(this.preamble, out);
199             writeBytes(CR_LF, out);
200         }
201         for (final MultipartPart part : getParts()) {
202             writeBytes(TWO_HYPHENS, out);
203             writeBytes(boundaryEncoded, out);
204             writeBytes(CR_LF, out);
205 
206             formatMultipartHeader(part, out);
207 
208             writeBytes(CR_LF, out);
209 
210             if (writeContent) {
211                 part.getBody().writeTo(out);
212             }
213             writeBytes(CR_LF, out);
214         }
215         writeBytes(TWO_HYPHENS, out);
216         writeBytes(boundaryEncoded, out);
217         writeBytes(TWO_HYPHENS, out);
218         writeBytes(CR_LF, out);
219         if (this.epilogue != null) {
220             writeBytes(this.epilogue, out);
221             writeBytes(CR_LF, out);
222         }
223     }
224 
225     /**
226      * Write the multipart header fields; depends on the style.
227      */
228     protected abstract void formatMultipartHeader(
229             final MultipartPart part,
230             final OutputStream out) throws IOException;
231 
232     /**
233      * Writes out the content in the multipart/form encoding. This method
234      * produces slightly different formatting depending on its compatibility
235      * mode.
236      */
237     public void writeTo(final OutputStream out) throws IOException {
238         doWriteTo(out, true);
239     }
240 
241     /**
242      * Determines the total length of the multipart content (content length of
243      * individual parts plus that of extra elements required to delimit the parts
244      * from one another). If any of the @{link BodyPart}s contained in this object
245      * is of a streaming entity of unknown length the total length is also unknown.
246      * <p>
247      * This method buffers only a small amount of data in order to determine the
248      * total length of the entire entity. The content of individual parts is not
249      * buffered.
250      * </p>
251      *
252      * @return total length of the multipart entity if known, {@code -1}
253      * otherwise.
254      */
255     public long getTotalLength() {
256         long contentLen = 0;
257         for (final MultipartPart part : getParts()) {
258             final ContentBody body = part.getBody();
259             final long len = body.getContentLength();
260             if (len >= 0) {
261                 contentLen += len;
262             } else {
263                 return -1;
264             }
265         }
266         final ByteArrayOutputStream out = new ByteArrayOutputStream();
267         try {
268             doWriteTo(out, false);
269             final byte[] extra = out.toByteArray();
270             return contentLen + extra.length;
271         } catch (final IOException ex) {
272             // Should never happen
273             return -1;
274         }
275     }
276 
277 }