1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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
115
116
117
118
119
120
121
122
123
124
125
126
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
135
136
137
138
139
140
141
142
143
144
145
146
147
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
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
241 String contentDisposition = "Content-Disposition: form-data; name=\"" + name + "\"";
242 if (fileName != null)
243 contentDisposition += "; filename=\"" + fileName + "\"";
244 contentDisposition += "\r\n";
245
246
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 buffer.write(field.getValue().getBytes(StandardCharsets.UTF_8));
275 buffer.write(CR_LF_BYTES);
276 }
277 buffer.write(CR_LF_BYTES);
278 return ByteBuffer.wrap(buffer.toByteArray());
279 }
280 catch (IOException x)
281 {
282 throw new RuntimeIOException(x);
283 }
284 }
285
286 @Override
287 public String toString()
288 {
289 return String.format("%s@%x[name=%s,fileName=%s,length=%d,headers=%s]",
290 getClass().getSimpleName(),
291 hashCode(),
292 name,
293 fileName,
294 content.getLength(),
295 fields);
296 }
297 }
298
299 private class MultiPartIterator implements Iterator<ByteBuffer>, Synchronizable, Callback, Closeable
300 {
301 private Iterator<ByteBuffer> iterator;
302 private int index;
303 private State state = State.FIRST_BOUNDARY;
304
305 @Override
306 public boolean hasNext()
307 {
308 return state != State.COMPLETE;
309 }
310
311 @Override
312 public ByteBuffer next()
313 {
314 while (true)
315 {
316 switch (state)
317 {
318 case FIRST_BOUNDARY:
319 {
320 if (parts.isEmpty())
321 {
322 state = State.COMPLETE;
323 return onlyBoundary.slice();
324 }
325 else
326 {
327 state = State.HEADERS;
328 return firstBoundary.slice();
329 }
330 }
331 case HEADERS:
332 {
333 Part part = parts.get(index);
334 ContentProvider content = part.content;
335 if (content instanceof AsyncContentProvider)
336 ((AsyncContentProvider)content).setListener(listener);
337 iterator = content.iterator();
338 state = State.CONTENT;
339 return part.headers.slice();
340 }
341 case CONTENT:
342 {
343 if (iterator.hasNext())
344 return iterator.next();
345 ++index;
346 if (index == parts.size())
347 state = State.LAST_BOUNDARY;
348 else
349 state = State.MIDDLE_BOUNDARY;
350 break;
351 }
352 case MIDDLE_BOUNDARY:
353 {
354 state = State.HEADERS;
355 return middleBoundary.slice();
356 }
357 case LAST_BOUNDARY:
358 {
359 state = State.COMPLETE;
360 return lastBoundary.slice();
361 }
362 case COMPLETE:
363 {
364 throw new NoSuchElementException();
365 }
366 }
367 }
368 }
369
370 @Override
371 public Object getLock()
372 {
373 if (iterator instanceof Synchronizable)
374 return ((Synchronizable)iterator).getLock();
375 return this;
376 }
377
378 @Override
379 public void succeeded()
380 {
381 if (iterator instanceof Callback)
382 ((Callback)iterator).succeeded();
383 }
384
385 @Override
386 public void failed(Throwable x)
387 {
388 if (iterator instanceof Callback)
389 ((Callback)iterator).failed(x);
390 }
391
392 @Override
393 public void close() throws IOException
394 {
395 if (iterator instanceof Closeable)
396 ((Closeable)iterator).close();
397 }
398 }
399
400 private enum State
401 {
402 FIRST_BOUNDARY, HEADERS, CONTENT, MIDDLE_BOUNDARY, LAST_BOUNDARY, COMPLETE
403 }
404 }