View Javadoc
1   /*
2    * Copyright 2020 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
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   * The multipart file builder implementation.
53   *
54   * @author Christian Bremer
55   */
56  public class MultipartFileBuilderImpl implements MultipartFileBuilder {
57  
58    private final File tmpDir;
59  
60    /**
61     * Instantiates a new multipart file builder.
62     */
63    public MultipartFileBuilderImpl() {
64      this((String) null);
65    }
66  
67    /**
68     * Instantiates a new multipart file builder.
69     *
70     * @param tmpDir the tmp dir
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     * Instantiates a new multipart file builder.
81     *
82     * @param tmpDir the tmp dir
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                   // Parsing form field as data url failed, treating value as plain/text.
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     //noinspection unchecked
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 }