View Javadoc
1   /*
2    * Copyright 2022 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.apiclient.webflux.contract.spring;
18  
19  import static java.util.Objects.nonNull;
20  
21  import java.lang.reflect.Method;
22  import java.util.List;
23  import java.util.Optional;
24  import java.util.function.Function;
25  import java.util.stream.Collectors;
26  import org.bremersee.apiclient.webflux.Invocation;
27  import org.bremersee.apiclient.webflux.InvocationParameter;
28  import org.reactivestreams.Publisher;
29  import org.springframework.core.ParameterizedTypeReference;
30  import org.springframework.core.ResolvableType;
31  import org.springframework.core.convert.converter.Converter;
32  import org.springframework.http.HttpEntity;
33  import org.springframework.http.MediaType;
34  import org.springframework.http.codec.multipart.Part;
35  import org.springframework.util.LinkedMultiValueMap;
36  import org.springframework.util.MultiValueMap;
37  import org.springframework.web.bind.annotation.RequestPart;
38  import org.springframework.web.reactive.function.BodyInserters;
39  import org.springframework.web.reactive.function.client.WebClient.RequestBodyUriSpec;
40  import org.springframework.web.reactive.function.client.WebClient.RequestHeadersUriSpec;
41  import reactor.core.publisher.Flux;
42  import reactor.core.publisher.Mono;
43  
44  /**
45   * The multipart data inserter.
46   *
47   * @author Christian Bremer
48   */
49  public class MultipartDataInserter extends AbstractRequestBodyInserter {
50  
51    private Function<Invocation, Optional<MediaType>> contentTypeResolver = new ContentTypeResolver();
52  
53    private Converter<Part, HttpEntity<?>> partConverter = new PartToHttpEntityConverter();
54  
55    /**
56     * With content type resolver.
57     *
58     * @param contentTypeResolver the content type resolver
59     * @return the multipart data inserter
60     */
61    public MultipartDataInserter withContentTypeResolver(
62        Function<Invocation, Optional<MediaType>> contentTypeResolver) {
63  
64      if (nonNull(contentTypeResolver)) {
65        this.contentTypeResolver = contentTypeResolver;
66      }
67      return this;
68    }
69  
70    /**
71     * With part converter.
72     *
73     * @param partConverter the part converter
74     * @return the multipart data inserter
75     */
76    public MultipartDataInserter withPartConverter(Converter<Part, HttpEntity<?>> partConverter) {
77      if (nonNull(partConverter)) {
78        this.partConverter = partConverter;
79      }
80      return this;
81    }
82  
83    @Override
84    public boolean canInsert(Invocation invocation) {
85      return isMultipartFormData(invocation) && super.canInsert(invocation);
86    }
87  
88    /**
89     * Is multipart form data.
90     *
91     * @param invocation the invocation
92     * @return the boolean
93     */
94    protected boolean isMultipartFormData(Invocation invocation) {
95      return contentTypeResolver.apply(invocation)
96          .filter(contentType -> contentType.isCompatibleWith(MediaType.MULTIPART_FORM_DATA))
97          .isPresent();
98    }
99  
100   @Override
101   protected boolean hasMappingAnnotation(InvocationParameter invocationParameter) {
102     return super.hasMappingAnnotation(invocationParameter)
103         || invocationParameter.hasParameterAnnotation(RequestPart.class);
104   }
105 
106   @Override
107   protected boolean isPossibleBodyValue(InvocationParameter invocationParameter) {
108     return isRequestBody(invocationParameter) || isRequestPart(invocationParameter);
109   }
110 
111   /**
112    * Is request body.
113    *
114    * @param invocationParameter the invocation parameter
115    * @return the boolean
116    */
117   protected boolean isRequestBody(InvocationParameter invocationParameter) {
118     Method method = invocationParameter.getMethod();
119     int index = invocationParameter.getIndex();
120     if (invocationParameter.getValue() instanceof MultiValueMap) {
121       return Optional.of(ResolvableType.forMethodParameter(method, index))
122           .filter(resolvableType -> resolvableType.getGenerics().length >= 2)
123           .map(resolvableType -> {
124             Class<?> r0 = resolvableType.resolveGeneric(0);
125             Class<?> r1 = resolvableType.resolveGeneric(1);
126             return nonNull(r0) && nonNull(r1)
127                 && String.class.isAssignableFrom(r0) && Part.class.isAssignableFrom(r1);
128           })
129           .isPresent();
130     } else if (invocationParameter.getValue() instanceof Publisher) {
131       return isMonoWithMultiValueMap(invocationParameter)
132           || isFluxWithPart(invocationParameter);
133     }
134     return false;
135   }
136 
137   private boolean isMonoWithMultiValueMap(InvocationParameter invocationParameter) {
138     Method method = invocationParameter.getMethod();
139     int index = invocationParameter.getIndex();
140     return invocationParameter.getValue() instanceof Mono && Optional
141         .of(ResolvableType.forMethodParameter(method, index))
142         .filter(ResolvableType::hasGenerics)
143         .map(resolvableType -> resolvableType.getGeneric(0))
144         .filter(resolvableType -> resolvableType.getGenerics().length >= 2)
145         .map(resolvableType -> {
146           Class<?> r0 = resolvableType.resolveGeneric(0);
147           Class<?> r1 = resolvableType.resolveGeneric(1);
148           return nonNull(r0) && nonNull(r1)
149               && String.class.isAssignableFrom(r0) && Part.class.isAssignableFrom(r1);
150         })
151         .isPresent();
152   }
153 
154   private boolean isFluxWithPart(InvocationParameter invocationParameter) {
155     Method method = invocationParameter.getMethod();
156     int index = invocationParameter.getIndex();
157     return invocationParameter.getValue() instanceof Flux && Optional
158         .of(ResolvableType.forMethodParameter(method, index))
159         .filter(ResolvableType::hasGenerics)
160         .map(resolvableType -> resolvableType.resolveGeneric(0))
161         .filter(Part.class::isAssignableFrom)
162         .isPresent();
163   }
164 
165   /**
166    * Is request part.
167    *
168    * @param invocationParameter the invocation parameter
169    * @return the boolean
170    */
171   protected boolean isRequestPart(InvocationParameter invocationParameter) {
172     return invocationParameter.hasParameterAnnotation(RequestPart.class)
173         && isPart(invocationParameter);
174   }
175 
176   private boolean isPart(InvocationParameter invocationParameter) {
177     if (invocationParameter.getValue() instanceof Part) {
178       return true;
179     } else if (invocationParameter.getValue() instanceof Publisher) {
180       Method method = invocationParameter.getMethod();
181       int index = invocationParameter.getIndex();
182       return Optional.of(ResolvableType.forMethodParameter(method, index))
183           .filter(ResolvableType::hasGenerics)
184           .map(resolvableType -> resolvableType.resolveGeneric(0))
185           .filter(Part.class::isAssignableFrom)
186           .isPresent();
187     }
188     return false;
189   }
190 
191   @Override
192   public RequestHeadersUriSpec<?> apply(Invocation invocation,
193       RequestBodyUriSpec requestBodyUriSpec) {
194     List<InvocationParameter> possibleBodies = findPossibleBodies(invocation);
195     List<Publisher<Part>> partPublishers = possibleBodies.stream()
196         .filter(invocationParameter -> isRequestPart(invocationParameter)
197             || isFluxWithPart(invocationParameter))
198         .map(invocationParameter -> toPublisher(invocationParameter.getValue()))
199         .collect(Collectors.toList());
200     Mono<MultiValueMap<String, HttpEntity<?>>> httpEntityMap;
201     if (!partPublishers.isEmpty()) {
202       httpEntityMap = toHttpEntityMap(partPublishers);
203     } else {
204       Publisher<MultiValueMap<String, Part>> partMap = findRequestBody(possibleBodies);
205       httpEntityMap = toHttpEntityMap(partMap);
206     }
207     //noinspection rawtypes
208     return (RequestHeadersUriSpec) requestBodyUriSpec.body(BodyInserters
209         .fromPublisher(httpEntityMap, new MultiValueMapTypeReference()));
210   }
211 
212   @SuppressWarnings("unchecked")
213   private Publisher<MultiValueMap<String, Part>> findRequestBody(
214       List<InvocationParameter> possibleBodies) {
215     return possibleBodies.stream()
216         .findFirst()
217         .map(InvocationParameter::getValue)
218         .map(value -> {
219           if (value instanceof Publisher) {
220             return (Publisher<MultiValueMap<String, Part>>) value;
221           } else {
222             MultiValueMap<String, Part> partMap = (MultiValueMap<String, Part>) value;
223             return Mono.just(partMap);
224           }
225         })
226         .orElseGet(Mono::empty);
227   }
228 
229   private Publisher<Part> toPublisher(Object value) {
230     Publisher<Part> partPublisher;
231     if (value instanceof Part) {
232       partPublisher = Mono.just((Part) value);
233     } else {
234       //noinspection unchecked
235       partPublisher = (Publisher<Part>) value;
236     }
237     return partPublisher;
238   }
239 
240   private Mono<MultiValueMap<String, HttpEntity<?>>> toHttpEntityMap(
241       List<Publisher<Part>> partPublishers) {
242     return Flux.concat(partPublishers)
243         .collect(
244             LinkedMultiValueMap::new,
245             (map, part) -> map.add(part.name(), partConverter.convert(part)));
246   }
247 
248   private Mono<MultiValueMap<String, HttpEntity<?>>> toHttpEntityMap(
249       Publisher<MultiValueMap<String, Part>> partMapPublisher) {
250 
251     return Flux.from(partMapPublisher)
252         .flatMap(partMap -> Flux.fromStream(partMap.values().stream()))
253         .flatMap(parts -> Flux.fromStream(parts.stream()))
254         .collect(
255             LinkedMultiValueMap::new,
256             (map, part) -> map.add(part.name(), partConverter.convert(part)));
257   }
258 
259   private static class MultiValueMapTypeReference
260       extends ParameterizedTypeReference<MultiValueMap<String, HttpEntity<?>>> {
261 
262   }
263 }