1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
46
47
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
57
58
59
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
72
73
74
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
90
91
92
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
113
114
115
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
167
168
169
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
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
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 }