View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.client.util;
20  
21  import java.io.ByteArrayOutputStream;
22  import java.io.Closeable;
23  import java.io.IOException;
24  import java.nio.ByteBuffer;
25  import java.nio.charset.StandardCharsets;
26  import java.util.ArrayList;
27  import java.util.Iterator;
28  import java.util.List;
29  import java.util.NoSuchElementException;
30  import java.util.Random;
31  import java.util.concurrent.atomic.AtomicBoolean;
32  
33  import org.eclipse.jetty.client.AsyncContentProvider;
34  import org.eclipse.jetty.client.Synchronizable;
35  import org.eclipse.jetty.client.api.ContentProvider;
36  import org.eclipse.jetty.http.HttpField;
37  import org.eclipse.jetty.http.HttpFields;
38  import org.eclipse.jetty.http.HttpHeader;
39  import org.eclipse.jetty.io.RuntimeIOException;
40  import org.eclipse.jetty.util.Callback;
41  import org.eclipse.jetty.util.log.Log;
42  import org.eclipse.jetty.util.log.Logger;
43  
44  /**
45   * <p>A {@link ContentProvider} for form uploads with the {@code "multipart/form-data"}
46   * content type.</p>
47   * <p>Example usage:</p>
48   * <pre>
49   * MultiPartContentProvider multiPart = new MultiPartContentProvider();
50   * multiPart.addFieldPart("field", new StringContentProvider("foo"), null);
51   * multiPart.addFilePart("icon", "img.png", new PathContentProvider(Paths.get("/tmp/img.png")), null);
52   * multiPart.close();
53   * ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
54   *         .method(HttpMethod.POST)
55   *         .content(multiPart)
56   *         .send();
57   * </pre>
58   * <p>The above example would be the equivalent of submitting this form:</p>
59   * <pre>
60   * &lt;form method="POST" enctype="multipart/form-data"  accept-charset="UTF-8"&gt;
61   *     &lt;input type="text" name="field" value="foo" /&gt;
62   *     &lt;input type="file" name="icon" /&gt;
63   * &lt;/form&gt;
64   * </pre>
65   */
66  public class MultiPartContentProvider extends AbstractTypedContentProvider implements AsyncContentProvider, Closeable
67  {
68      private static final Logger LOG = Log.getLogger(MultiPartContentProvider.class);
69      private static final byte[] COLON_SPACE_BYTES = new byte[]{':', ' '};
70      private static final byte[] CR_LF_BYTES = new byte[]{'\r', '\n'};
71  
72      private final List<Part> parts = new ArrayList<>();
73      private final ByteBuffer firstBoundary;
74      private final ByteBuffer middleBoundary;
75      private final ByteBuffer onlyBoundary;
76      private final ByteBuffer lastBoundary;
77      private final AtomicBoolean closed = new AtomicBoolean();
78      private Listener listener;
79      private long length = -1;
80  
81      public MultiPartContentProvider()
82      {
83          this(makeBoundary());
84      }
85  
86      public MultiPartContentProvider(String boundary)
87      {
88          super("multipart/form-data; boundary=" + boundary);
89          String firstBoundaryLine = "--" + boundary + "\r\n";
90          this.firstBoundary = ByteBuffer.wrap(firstBoundaryLine.getBytes(StandardCharsets.US_ASCII));
91          String middleBoundaryLine = "\r\n" + firstBoundaryLine;
92          this.middleBoundary = ByteBuffer.wrap(middleBoundaryLine.getBytes(StandardCharsets.US_ASCII));
93          String onlyBoundaryLine = "--" + boundary + "--\r\n";
94          this.onlyBoundary = ByteBuffer.wrap(onlyBoundaryLine.getBytes(StandardCharsets.US_ASCII));
95          String lastBoundaryLine = "\r\n" + onlyBoundaryLine;
96          this.lastBoundary = ByteBuffer.wrap(lastBoundaryLine.getBytes(StandardCharsets.US_ASCII));
97      }
98  
99      private static String makeBoundary()
100     {
101         Random random = new Random();
102         StringBuilder builder = new StringBuilder("JettyHttpClientBoundary");
103         int length = builder.length();
104         while (builder.length() < length + 16)
105         {
106             long rnd = random.nextLong();
107             builder.append(Long.toString(rnd < 0 ? -rnd : rnd, 36));
108         }
109         builder.setLength(length + 16);
110         return builder.toString();
111     }
112 
113     /**
114      * <p>Adds a field part with the given {@code name} as field name, and the given
115      * {@code content} as part content.</p>
116      * <p>The {@code Content-Type} of this part will be obtained from:</p>
117      * <ul>
118      *     <li>the {@code Content-Type} header in the {@code fields} parameter; otherwise</li>
119      *     <li>the {@link org.eclipse.jetty.client.api.ContentProvider.Typed#getContentType()} method if the {@code content} parameter
120      *     implements {@link org.eclipse.jetty.client.api.ContentProvider.Typed}; otherwise</li>
121      *     <li>"text/plain"</li>
122      * </ul>
123      *
124      * @param name the part name
125      * @param content the part content
126      * @param fields the headers associated with this part
127      */
128     public void addFieldPart(String name, ContentProvider content, HttpFields fields)
129     {
130         addPart(new Part(name, null, "text/plain", content, fields));
131     }
132 
133     /**
134      * <p>Adds a file part with the given {@code name} as field name, the given
135      * {@code fileName} as file name, and the given {@code content} as part content.</p>
136      * <p>The {@code Content-Type} of this part will be obtained from:</p>
137      * <ul>
138      *     <li>the {@code Content-Type} header in the {@code fields} parameter; otherwise</li>
139      *     <li>the {@link org.eclipse.jetty.client.api.ContentProvider.Typed#getContentType()} method if the {@code content} parameter
140      *     implements {@link org.eclipse.jetty.client.api.ContentProvider.Typed}; otherwise</li>
141      *     <li>"application/octet-stream"</li>
142      * </ul>
143      *
144      * @param name the part name
145      * @param fileName the file name associated to this part
146      * @param content the part content
147      * @param fields the headers associated with this part
148      */
149     public void addFilePart(String name, String fileName, ContentProvider content, HttpFields fields)
150     {
151         addPart(new Part(name, fileName, "application/octet-stream", content, fields));
152     }
153 
154     private void addPart(Part part)
155     {
156         parts.add(part);
157         if (LOG.isDebugEnabled())
158             LOG.debug("Added {}", part);
159     }
160 
161     @Override
162     public void setListener(Listener listener)
163     {
164         this.listener = listener;
165         if (closed.get())
166             this.length = calculateLength();
167     }
168 
169     private long calculateLength()
170     {
171         // Compute the length, if possible.
172         if (parts.isEmpty())
173         {
174             return onlyBoundary.remaining();
175         }
176         else
177         {
178             long result = 0;
179             for (int i = 0; i < parts.size(); ++i)
180             {
181                 result += (i == 0) ? firstBoundary.remaining() : middleBoundary.remaining();
182                 Part part = parts.get(i);
183                 long partLength = part.length;
184                 result += partLength;
185                 if (partLength < 0)
186                 {
187                     result = -1;
188                     break;
189                 }
190             }
191             if (result > 0)
192                 result += lastBoundary.remaining();
193             return result;
194         }
195     }
196 
197     @Override
198     public long getLength()
199     {
200         return length;
201     }
202 
203     @Override
204     public Iterator<ByteBuffer> iterator()
205     {
206         return new MultiPartIterator();
207     }
208 
209     @Override
210     public void close()
211     {
212         closed.compareAndSet(false, true);
213     }
214 
215     private static class Part
216     {
217         private final String name;
218         private final String fileName;
219         private final String contentType;
220         private final ContentProvider content;
221         private final HttpFields fields;
222         private final ByteBuffer headers;
223         private final long length;
224 
225         private Part(String name, String fileName, String contentType, ContentProvider content, HttpFields fields)
226         {
227             this.name = name;
228             this.fileName = fileName;
229             this.contentType = contentType;
230             this.content = content;
231             this.fields = fields;
232             this.headers = headers();
233             this.length = content.getLength() < 0 ? -1 : headers.remaining() + content.getLength();
234         }
235 
236         private ByteBuffer headers()
237         {
238             try
239             {
240                 // Compute the Content-Disposition.
241                 String contentDisposition = "Content-Disposition: form-data; name=\"" + name + "\"";
242                 if (fileName != null)
243                     contentDisposition += "; filename=\"" + fileName + "\"";
244                 contentDisposition += "\r\n";
245 
246                 // Compute the Content-Type.
247                 String contentType = fields == null ? null : fields.get(HttpHeader.CONTENT_TYPE);
248                 if (contentType == null)
249                 {
250                     if (content instanceof Typed)
251                         contentType = ((Typed)content).getContentType();
252                     else
253                         contentType = this.contentType;
254                 }
255                 contentType = "Content-Type: " + contentType + "\r\n";
256 
257                 if (fields == null || fields.size() == 0)
258                 {
259                     String headers = contentDisposition;
260                     headers += contentType;
261                     headers += "\r\n";
262                     return ByteBuffer.wrap(headers.getBytes(StandardCharsets.UTF_8));
263                 }
264 
265                 ByteArrayOutputStream buffer = new ByteArrayOutputStream((fields.size() + 1) * contentDisposition.length());
266                 buffer.write(contentDisposition.getBytes(StandardCharsets.UTF_8));
267                 buffer.write(contentType.getBytes(StandardCharsets.UTF_8));
268                 for (HttpField field : fields)
269                 {
270                     if (HttpHeader.CONTENT_TYPE.equals(field.getHeader()))
271                         continue;
272                     buffer.write(field.getName().getBytes(StandardCharsets.US_ASCII));
273                     buffer.write(COLON_SPACE_BYTES);
274                     String value = field.getValue();
275                     if (value != null)
276                         buffer.write(value.getBytes(StandardCharsets.UTF_8));
277                     buffer.write(CR_LF_BYTES);
278                 }
279                 buffer.write(CR_LF_BYTES);
280                 return ByteBuffer.wrap(buffer.toByteArray());
281             }
282             catch (IOException x)
283             {
284                 throw new RuntimeIOException(x);
285             }
286         }
287 
288         @Override
289         public String toString()
290         {
291             return String.format("%s@%x[name=%s,fileName=%s,length=%d,headers=%s]",
292                     getClass().getSimpleName(),
293                     hashCode(),
294                     name,
295                     fileName,
296                     content.getLength(),
297                     fields);
298         }
299     }
300 
301     private class MultiPartIterator implements Iterator<ByteBuffer>, Synchronizable, Callback, Closeable
302     {
303         private Iterator<ByteBuffer> iterator;
304         private int index;
305         private State state = State.FIRST_BOUNDARY;
306 
307         @Override
308         public boolean hasNext()
309         {
310             return state != State.COMPLETE;
311         }
312 
313         @Override
314         public ByteBuffer next()
315         {
316             while (true)
317             {
318                 switch (state)
319                 {
320                     case FIRST_BOUNDARY:
321                     {
322                         if (parts.isEmpty())
323                         {
324                             state = State.COMPLETE;
325                             return onlyBoundary.slice();
326                         }
327                         else
328                         {
329                             state = State.HEADERS;
330                             return firstBoundary.slice();
331                         }
332                     }
333                     case HEADERS:
334                     {
335                         Part part = parts.get(index);
336                         ContentProvider content = part.content;
337                         if (content instanceof AsyncContentProvider)
338                             ((AsyncContentProvider)content).setListener(listener);
339                         iterator = content.iterator();
340                         state = State.CONTENT;
341                         return part.headers.slice();
342                     }
343                     case CONTENT:
344                     {
345                         if (iterator.hasNext())
346                             return iterator.next();
347                         ++index;
348                         if (index == parts.size())
349                             state = State.LAST_BOUNDARY;
350                         else
351                             state = State.MIDDLE_BOUNDARY;
352                         break;
353                     }
354                     case MIDDLE_BOUNDARY:
355                     {
356                         state = State.HEADERS;
357                         return middleBoundary.slice();
358                     }
359                     case LAST_BOUNDARY:
360                     {
361                         state = State.COMPLETE;
362                         return lastBoundary.slice();
363                     }
364                     case COMPLETE:
365                     {
366                         throw new NoSuchElementException();
367                     }
368                 }
369             }
370         }
371 
372         @Override
373         public Object getLock()
374         {
375             if (iterator instanceof Synchronizable)
376                 return ((Synchronizable)iterator).getLock();
377             return this;
378         }
379 
380         @Override
381         public void succeeded()
382         {
383             if (iterator instanceof Callback)
384                 ((Callback)iterator).succeeded();
385         }
386 
387         @Override
388         public void failed(Throwable x)
389         {
390             if (iterator instanceof Callback)
391                 ((Callback)iterator).failed(x);
392         }
393 
394         @Override
395         public void close() throws IOException
396         {
397             if (iterator instanceof Closeable)
398                 ((Closeable)iterator).close();
399         }
400     }
401 
402     private enum State
403     {
404         FIRST_BOUNDARY, HEADERS, CONTENT, MIDDLE_BOUNDARY, LAST_BOUNDARY, COMPLETE
405     }
406 }