/
SynchronossPartHttpMessageReader.java
376 lines (307 loc) · 11.4 KB
/
SynchronossPartHttpMessageReader.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.codec.multipart;
import java.io.IOException;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import org.synchronoss.cloud.nio.multipart.DefaultPartBodyStreamStorageFactory;
import org.synchronoss.cloud.nio.multipart.Multipart;
import org.synchronoss.cloud.nio.multipart.MultipartContext;
import org.synchronoss.cloud.nio.multipart.MultipartUtils;
import org.synchronoss.cloud.nio.multipart.NioMultipartParser;
import org.synchronoss.cloud.nio.multipart.NioMultipartParserListener;
import org.synchronoss.cloud.nio.multipart.PartBodyStreamStorageFactory;
import org.synchronoss.cloud.nio.stream.storage.StreamStorage;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink;
import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ReactiveHttpInputMessage;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* {@code HttpMessageReader} for parsing {@code "multipart/form-data"} requests
* to a stream of {@link Part}'s using the Synchronoss NIO Multipart library.
*
* <p>This reader can be provided to {@link MultipartHttpMessageReader} in order
* to aggregate all parts into a Map.
*
* @author Sebastien Deleuze
* @author Rossen Stoyanchev
* @author Arjen Poutsma
* @since 5.0
* @see <a href="https://github.com/synchronoss/nio-multipart">Synchronoss NIO Multipart</a>
* @see MultipartHttpMessageReader
*/
public class SynchronossPartHttpMessageReader implements HttpMessageReader<Part> {
private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory();
private final PartBodyStreamStorageFactory streamStorageFactory = new DefaultPartBodyStreamStorageFactory();
@Override
public List<MediaType> getReadableMediaTypes() {
return Collections.singletonList(MediaType.MULTIPART_FORM_DATA);
}
@Override
public boolean canRead(ResolvableType elementType, @Nullable MediaType mediaType) {
return Part.class.equals(elementType.resolve(Object.class)) &&
(mediaType == null || MediaType.MULTIPART_FORM_DATA.isCompatibleWith(mediaType));
}
@Override
public Flux<Part> read(ResolvableType elementType, ReactiveHttpInputMessage message, Map<String, Object> hints) {
return Flux.create(new SynchronossPartGenerator(message, this.bufferFactory, this.streamStorageFactory));
}
@Override
public Mono<Part> readMono(ResolvableType elementType, ReactiveHttpInputMessage message, Map<String, Object> hints) {
return Mono.error(new UnsupportedOperationException("Cannot read multipart request body into single Part"));
}
/**
* Consume and feed input to the Synchronoss parser, then listen for parser
* output events and adapt to {@code Flux<Sink<Part>>}.
*/
private static class SynchronossPartGenerator implements Consumer<FluxSink<Part>> {
private final ReactiveHttpInputMessage inputMessage;
private final DataBufferFactory bufferFactory;
private final PartBodyStreamStorageFactory streamStorageFactory;
SynchronossPartGenerator(ReactiveHttpInputMessage inputMessage, DataBufferFactory bufferFactory,
PartBodyStreamStorageFactory streamStorageFactory) {
this.inputMessage = inputMessage;
this.bufferFactory = bufferFactory;
this.streamStorageFactory = streamStorageFactory;
}
@Override
public void accept(FluxSink<Part> emitter) {
HttpHeaders headers = this.inputMessage.getHeaders();
MediaType mediaType = headers.getContentType();
Assert.state(mediaType != null, "No content type set");
int length = Math.toIntExact(headers.getContentLength());
Charset charset = Optional.ofNullable(mediaType.getCharset()).orElse(StandardCharsets.UTF_8);
MultipartContext context = new MultipartContext(mediaType.toString(), length, charset.name());
NioMultipartParserListener listener = new FluxSinkAdapterListener(emitter, this.bufferFactory, context);
NioMultipartParser parser = Multipart
.multipart(context)
.usePartBodyStreamStorageFactory(streamStorageFactory)
.forNIO(listener);
this.inputMessage.getBody().subscribe(buffer -> {
byte[] resultBytes = new byte[buffer.readableByteCount()];
buffer.read(resultBytes);
try {
parser.write(resultBytes);
}
catch (IOException ex) {
listener.onError("Exception thrown providing input to the parser", ex);
}
finally {
DataBufferUtils.release(buffer);
}
}, ex -> {
try {
listener.onError("Request body input error", ex);
parser.close();
}
catch (IOException ex2) {
listener.onError("Exception thrown while closing the parser", ex2);
}
}, () -> {
try {
parser.close();
}
catch (IOException ex) {
listener.onError("Exception thrown while closing the parser", ex);
}
});
}
}
/**
* Listen for parser output and adapt to {@code Flux<Sink<Part>>}.
*/
private static class FluxSinkAdapterListener implements NioMultipartParserListener {
private final FluxSink<Part> sink;
private final DataBufferFactory bufferFactory;
private final MultipartContext context;
private final AtomicInteger terminated = new AtomicInteger(0);
FluxSinkAdapterListener(FluxSink<Part> sink, DataBufferFactory factory, MultipartContext context) {
this.sink = sink;
this.bufferFactory = factory;
this.context = context;
}
@Override
public void onPartFinished(StreamStorage storage, Map<String, List<String>> headers) {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(headers);
this.sink.next(createPart(storage, httpHeaders));
}
private Part createPart(StreamStorage storage, HttpHeaders httpHeaders) {
String filename = MultipartUtils.getFileName(httpHeaders);
if (filename != null) {
return new SynchronossFilePart(httpHeaders, filename, storage, this.bufferFactory);
}
else if (MultipartUtils.isFormField(httpHeaders, this.context)) {
String value = MultipartUtils.readFormParameterValue(storage, httpHeaders);
return new SynchronossFormFieldPart(httpHeaders, this.bufferFactory, value);
}
else {
return new SynchronossPart(httpHeaders, storage, this.bufferFactory);
}
}
@Override
public void onError(String message, Throwable cause) {
if (this.terminated.getAndIncrement() == 0) {
this.sink.error(new RuntimeException(message, cause));
}
}
@Override
public void onAllPartsFinished() {
if (this.terminated.getAndIncrement() == 0) {
this.sink.complete();
}
}
@Override
public void onNestedPartStarted(Map<String, List<String>> headersFromParentPart) {
}
@Override
public void onNestedPartFinished() {
}
}
private abstract static class AbstractSynchronossPart implements Part {
private final String name;
private final HttpHeaders headers;
private final DataBufferFactory bufferFactory;
AbstractSynchronossPart(HttpHeaders headers, DataBufferFactory bufferFactory) {
Assert.notNull(headers, "HttpHeaders is required");
Assert.notNull(bufferFactory, "DataBufferFactory is required");
this.name = MultipartUtils.getFieldName(headers);
this.headers = headers;
this.bufferFactory = bufferFactory;
}
@Override
public String name() {
return this.name;
}
@Override
public HttpHeaders headers() {
return this.headers;
}
DataBufferFactory getBufferFactory() {
return this.bufferFactory;
}
}
private static class SynchronossPart extends AbstractSynchronossPart {
private final StreamStorage storage;
SynchronossPart(HttpHeaders headers, StreamStorage storage, DataBufferFactory factory) {
super(headers, factory);
Assert.notNull(storage, "StreamStorage is required");
this.storage = storage;
}
@Override
public Flux<DataBuffer> content() {
return DataBufferUtils.readInputStream(getStorage()::getInputStream, getBufferFactory(), 4096);
}
protected StreamStorage getStorage() {
return this.storage;
}
}
private static class SynchronossFilePart extends SynchronossPart implements FilePart {
private static final OpenOption[] FILE_CHANNEL_OPTIONS =
{StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE};
private final String filename;
SynchronossFilePart(HttpHeaders headers, String filename, StreamStorage storage, DataBufferFactory factory) {
super(headers, storage, factory);
this.filename = filename;
}
@Override
public String filename() {
return this.filename;
}
@Override
public Mono<Void> transferTo(Path dest) {
ReadableByteChannel input = null;
FileChannel output = null;
try {
input = Channels.newChannel(getStorage().getInputStream());
output = FileChannel.open(dest, FILE_CHANNEL_OPTIONS);
long size = (input instanceof FileChannel ? ((FileChannel) input).size() : Long.MAX_VALUE);
long totalWritten = 0;
while (totalWritten < size) {
long written = output.transferFrom(input, totalWritten, size - totalWritten);
if (written <= 0) {
break;
}
totalWritten += written;
}
}
catch (IOException ex) {
return Mono.error(ex);
}
finally {
if (input != null) {
try {
input.close();
}
catch (IOException ignored) {
}
}
if (output != null) {
try {
output.close();
}
catch (IOException ignored) {
}
}
}
return Mono.empty();
}
}
private static class SynchronossFormFieldPart extends AbstractSynchronossPart implements FormFieldPart {
private final String content;
SynchronossFormFieldPart(HttpHeaders headers, DataBufferFactory bufferFactory, String content) {
super(headers, bufferFactory);
this.content = content;
}
@Override
public String value() {
return this.content;
}
@Override
public Flux<DataBuffer> content() {
byte[] bytes = this.content.getBytes(getCharset());
DataBuffer buffer = getBufferFactory().allocateBuffer(bytes.length);
buffer.write(bytes);
return Flux.just(buffer);
}
private Charset getCharset() {
String name = MultipartUtils.getCharEncoding(headers());
return (name != null ? Charset.forName(name) : StandardCharsets.UTF_8);
}
}
}