1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.bremersee.spring.web.reactive.multipart;
18
19 import static java.util.Objects.isNull;
20 import static java.util.Objects.nonNull;
21 import static org.springframework.util.ObjectUtils.isEmpty;
22
23 import eu.maxschuster.dataurl.DataUrl;
24 import eu.maxschuster.dataurl.DataUrlSerializer;
25 import java.io.ByteArrayInputStream;
26 import java.io.File;
27 import java.io.IOException;
28 import java.nio.charset.StandardCharsets;
29 import java.util.Collections;
30 import java.util.LinkedHashMap;
31 import java.util.List;
32 import java.util.Map;
33 import java.util.Optional;
34 import org.bremersee.spring.web.multipart.FileAwareMultipartFile;
35 import org.springframework.core.io.buffer.DataBuffer;
36 import org.springframework.http.HttpHeaders;
37 import org.springframework.http.MediaType;
38 import org.springframework.http.codec.multipart.FilePart;
39 import org.springframework.http.codec.multipart.FormFieldPart;
40 import org.springframework.http.codec.multipart.Part;
41 import org.springframework.lang.NonNull;
42 import org.springframework.util.LinkedMultiValueMap;
43 import org.springframework.util.MimeType;
44 import org.springframework.util.MultiValueMap;
45 import org.springframework.web.multipart.MultipartException;
46 import org.springframework.web.multipart.MultipartFile;
47 import reactor.core.publisher.Flux;
48 import reactor.core.publisher.Mono;
49 import reactor.util.function.Tuple2;
50
51
52
53
54
55
56 public class MultipartFileBuilderImpl implements MultipartFileBuilder {
57
58 private final File tmpDir;
59
60
61
62
63 public MultipartFileBuilderImpl() {
64 this((String) null);
65 }
66
67
68
69
70
71
72 public MultipartFileBuilderImpl(String tmpDir) {
73 this(Optional.ofNullable(tmpDir)
74 .filter(str -> !str.isBlank())
75 .map(File::new)
76 .orElseGet(() -> new File(System.getProperty("java.io.tmpdir"))));
77 }
78
79
80
81
82
83
84 public MultipartFileBuilderImpl(File tmpDir) {
85 this.tmpDir = Optional.ofNullable(tmpDir)
86 .filter(dir -> dir.exists() && dir.isDirectory() && dir.canRead() && dir.canWrite())
87 .orElseGet(() -> new File(System.getProperty("java.io.tmpdir")));
88 }
89
90 private Mono<MultipartFile> build(FilePart filePart) {
91 return Optional.ofNullable(filePart)
92 .map(fp -> {
93 File file = newTmpFile();
94 return filePart.transferTo(file)
95 .then(Mono.just(new FileAwareMultipartFile(
96 file,
97 filePart.name(),
98 filePart.filename(),
99 Optional.of(filePart)
100 .map(part -> part.headers().getContentType())
101 .map(MimeType::toString)
102 .orElse(MediaType.APPLICATION_OCTET_STREAM_VALUE))));
103 })
104 .orElseGet(() -> Mono.just(FileAwareMultipartFile.empty()))
105 .cast(MultipartFile.class);
106 }
107
108 private Mono<MultipartFile> build(FormFieldPart formFieldPart) {
109 return Optional.ofNullable(formFieldPart)
110 .filter(ffp -> !isEmpty(ffp.value()))
111 .map(ffp -> Mono.just(ffp)
112 .handle((part, sink) -> {
113 String value = part.value();
114 byte[] data = null;
115 String contentType = null;
116 if (value.toLowerCase().startsWith("data:") && value.indexOf(',') > -1) {
117 DataUrl dataUrl = null;
118 try {
119 dataUrl = new DataUrlSerializer().unserialize(value);
120 } catch (Exception ignored) {
121
122 }
123 if (nonNull(dataUrl)) {
124 data = dataUrl.getData();
125 contentType = dataUrl.getMimeType();
126 }
127 }
128 if (isNull(data)) {
129 data = value.getBytes(StandardCharsets.UTF_8);
130 contentType = MediaType.TEXT_PLAIN_VALUE;
131 }
132 try {
133 sink.next(new FileAwareMultipartFile(
134 new ByteArrayInputStream(data),
135 tmpDir,
136 part.name(),
137 null,
138 contentType));
139
140 } catch (IOException e) {
141 sink.error(new MultipartException(
142 "Creating multipart file from form field part failed.", e));
143 }
144 }))
145 .orElseGet(() -> Mono.just(FileAwareMultipartFile.empty()))
146 .cast(MultipartFile.class);
147 }
148
149 @Override
150 public Mono<MultipartFile> build(Part part) {
151 if (part instanceof FilePart) {
152 return build((FilePart) part);
153 }
154 if (part instanceof FormFieldPart) {
155 return build((FormFieldPart) part);
156 }
157 return Mono.just(FileAwareMultipartFile.empty());
158 }
159
160 @Override
161 public Mono<MultipartFile> build(Flux<? extends Part> parts) {
162
163 return ((Flux<Part>) parts)
164 .last(new EmptyPart())
165 .flatMap(this::build);
166 }
167
168 @Override
169 public Mono<List<MultipartFile>> buildList(Flux<? extends Part> parts) {
170 return parts.flatMap(this::build)
171 .collectList();
172 }
173
174 @Override
175 public Mono<List<MultipartFile>> buildList(
176 MultiValueMap<String, Part> multiPartData, String... requestParameters) {
177
178 return Flux.fromArray(Optional.ofNullable(requestParameters).orElseGet(() -> new String[0]))
179 .flatMap(requestParameter -> build(multiPartData.getFirst(requestParameter)))
180 .collectList();
181 }
182
183 @Override
184 public Flux<List<MultipartFile>> buildLists(
185 MultiValueMap<String, Part> multiPartData, String... requestParameters) {
186
187 return Flux.fromArray(Optional.ofNullable(requestParameters).orElseGet(() -> new String[0]))
188 .flatMap(reqParam -> createList(multiPartData, reqParam));
189 }
190
191 @SuppressWarnings("unchecked")
192 @Override
193 public Mono<Map<String, MultipartFile>> buildMap(Flux<? extends Part>... parts) {
194 return Flux.fromArray(parts)
195 .flatMap(this::build)
196 .collectMap(MultipartFile::getName, multipartFile -> multipartFile, LinkedHashMap::new);
197 }
198
199 @Override
200 public Mono<Map<String, MultipartFile>> buildMap(
201 MultiValueMap<String, Part> multiPartData, String... requestParameters) {
202
203 return Flux.fromArray(Optional.ofNullable(requestParameters).orElseGet(() -> new String[0]))
204 .flatMap(reqParam -> build(multiPartData.getFirst(reqParam))
205 .zipWith(Mono.just(reqParam)))
206 .collectMap(Tuple2::getT2, Tuple2::getT1, LinkedHashMap::new);
207 }
208
209 @SuppressWarnings("unchecked")
210 @Override
211 public Mono<MultiValueMap<String, MultipartFile>> buildMultiValueMap(
212 Flux<? extends Part>... parts) {
213 return Flux.fromArray(parts)
214 .flatMap(this::buildList)
215 .filter(list -> !list.isEmpty())
216 .collectMap(list -> list.get(0).getName(), list -> list, LinkedHashMap::new)
217 .map(LinkedMultiValueMap::new);
218 }
219
220 @Override
221 public Mono<MultiValueMap<String, MultipartFile>> buildMultiValueMap(
222 MultiValueMap<String, Part> multiPartData, String... requestParameters) {
223
224 return Flux.fromArray(Optional.ofNullable(requestParameters).orElseGet(() -> new String[0]))
225 .flatMap(reqParam -> createList(multiPartData, reqParam)
226 .zipWith(Mono.just(reqParam)))
227 .collectMap(Tuple2::getT2, Tuple2::getT1, LinkedHashMap::new)
228 .map(LinkedMultiValueMap::new);
229 }
230
231 private File newTmpFile() {
232 try {
233 return File.createTempFile("uploaded-", ".tmp", tmpDir);
234 } catch (Exception e) {
235 throw new MultipartException("Creating tmp file failed.", e);
236 }
237 }
238
239 private Mono<List<MultipartFile>> createList(
240 MultiValueMap<String, Part> multiPartData,
241 String requestParameter) {
242
243 List<Part> contentParts = multiPartData
244 .getOrDefault(requestParameter, Collections.emptyList());
245 return contentParts.isEmpty()
246 ? Mono.empty()
247 : Flux.fromIterable(contentParts).flatMap(this::build).collectList();
248 }
249
250 private static class EmptyPart implements Part {
251
252 private EmptyPart() {
253 }
254
255 @NonNull
256 @Override
257 public String name() {
258 return "";
259 }
260
261 @NonNull
262 @Override
263 public HttpHeaders headers() {
264 return new HttpHeaders();
265 }
266
267 @NonNull
268 @Override
269 public Flux<DataBuffer> content() {
270 return Flux.empty();
271 }
272 }
273
274 }