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 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 }