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      static ByteArrayBuffer encode(
50              final Charset charset, final CharSequence string) {
51          final ByteBuffer encoded = charset.encode(CharBuffer.wrap(string));
52          final ByteArrayBuffer bab = new ByteArrayBuffer(encoded.remaining());
53          bab.append(encoded.array(), encoded.arrayOffset() + encoded.position(), encoded.remaining());
54          return bab;
55      }
56  
57      static void writeBytes(
58              final ByteArrayBuffer b, final OutputStream out) throws IOException {
59          out.write(b.array(), 0, b.length());
60      }
61  
62      static void writeBytes(
63              final CharSequence s, final Charset charset, final OutputStream out) throws IOException {
64          final ByteArrayBuffer b = encode(charset, s);
65          writeBytes(b, out);
66      }
67  
68      static void writeBytes(
69              final CharSequence s, final OutputStream out) throws IOException {
70          final ByteArrayBuffer b = encode(StandardCharsets.ISO_8859_1, s);
71          writeBytes(b, out);
72      }
73  
74      static boolean isLineBreak(final char ch) {
75          return ch == '\r' || ch == '\n' || ch == '\f' || ch == 11;
76      }
77  
78      static CharSequence stripLineBreaks(final CharSequence s) {
79          if (s == null) {
80              return null;
81          }
82          boolean requiresRewrite = false;
83          int n = 0;
84          for (; n < s.length(); n++) {
85              final char ch = s.charAt(n);
86              if (isLineBreak(ch)) {
87                  requiresRewrite = true;
88                  break;
89              }
90          }
91          if (!requiresRewrite) {
92              return s;
93          }
94          final StringBuilder buf = new StringBuilder();
95          buf.append(s, 0, n);
96          for (; n < s.length(); n++) {
97              final char ch = s.charAt(n);
98              if (isLineBreak(ch)) {
99                  buf.append(' ');
100             } else {
101                 buf.append(ch);
102             }
103         }
104         return buf.toString();
105     }
106 
107     static void writeField(
108             final MimeField field, final OutputStream out) throws IOException {
109         writeBytes(stripLineBreaks(field.getName()), out);
110         writeBytes(FIELD_SEP, out);
111         writeBytes(stripLineBreaks(field.getBody()), out);
112         writeBytes(CR_LF, out);
113     }
114 
115     static void writeField(
116             final MimeField field, final Charset charset, final OutputStream out) throws IOException {
117         writeBytes(stripLineBreaks(field.getName()), charset, out);
118         writeBytes(FIELD_SEP, out);
119         writeBytes(stripLineBreaks(field.getBody()), charset, out);
120         writeBytes(CR_LF, out);
121     }
122 
123     static final ByteArrayBuffer FIELD_SEP = encode(StandardCharsets.ISO_8859_1, ": ");
124     static final ByteArrayBuffer CR_LF = encode(StandardCharsets.ISO_8859_1, "\r\n");
125     static final ByteArrayBuffer TWO_HYPHENS = encode(StandardCharsets.ISO_8859_1, "--");
126 
127     final Charset charset;
128     final String boundary;
129 
130     /**
131      * Creates an instance with the specified settings.
132      *
133      * @param charset the character set to use. May be {@code null}, in which case {@link StandardCharsets#ISO_8859_1} 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 AbstractMultipartFormat(final Charset charset, final String boundary) {
138         super();
139         Args.notNull(boundary, "Multipart boundary");
140         this.charset = charset != null ? charset : StandardCharsets.ISO_8859_1;
141         this.boundary = boundary;
142     }
143 
144     public AbstractMultipartFormat(final String boundary) {
145         this(null, boundary);
146     }
147 
148     public abstract List<MultipartPart> getParts();
149 
150     void doWriteTo(
151         final OutputStream out,
152         final boolean writeContent) throws IOException {
153 
154         final ByteArrayBuffer boundaryEncoded = encode(this.charset, this.boundary);
155         for (final MultipartPart part: getParts()) {
156             writeBytes(TWO_HYPHENS, out);
157             writeBytes(boundaryEncoded, out);
158             writeBytes(CR_LF, out);
159 
160             formatMultipartHeader(part, out);
161 
162             writeBytes(CR_LF, out);
163 
164             if (writeContent) {
165                 part.getBody().writeTo(out);
166             }
167             writeBytes(CR_LF, out);
168         }
169         writeBytes(TWO_HYPHENS, out);
170         writeBytes(boundaryEncoded, out);
171         writeBytes(TWO_HYPHENS, out);
172         writeBytes(CR_LF, out);
173     }
174 
175     /**
176       * Write the multipart header fields; depends on the style.
177       */
178     protected abstract void formatMultipartHeader(
179         final MultipartPart part,
180         final OutputStream out) throws IOException;
181 
182     /**
183      * Writes out the content in the multipart/form encoding. This method
184      * produces slightly different formatting depending on its compatibility
185      * mode.
186      */
187     public void writeTo(final OutputStream out) throws IOException {
188         doWriteTo(out, true);
189     }
190 
191     /**
192      * Determines the total length of the multipart content (content length of
193      * individual parts plus that of extra elements required to delimit the parts
194      * from one another). If any of the @{link BodyPart}s contained in this object
195      * is of a streaming entity of unknown length the total length is also unknown.
196      * <p>
197      * This method buffers only a small amount of data in order to determine the
198      * total length of the entire entity. The content of individual parts is not
199      * buffered.
200      * </p>
201      *
202      * @return total length of the multipart entity if known, {@code -1}
203      *   otherwise.
204      */
205     public long getTotalLength() {
206         long contentLen = 0;
207         for (final MultipartPart part: getParts()) {
208             final ContentBody body = part.getBody();
209             final long len = body.getContentLength();
210             if (len >= 0) {
211                 contentLen += len;
212             } else {
213                 return -1;
214             }
215         }
216         final ByteArrayOutputStream out = new ByteArrayOutputStream();
217         try {
218             doWriteTo(out, false);
219             final byte[] extra = out.toByteArray();
220             return contentLen + extra.length;
221         } catch (final IOException ex) {
222             // Should never happen
223             return -1;
224         }
225     }
226 
227 }