Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
reactivefeign.methodhandler.PublisherClientMethodHandler Maven / Gradle / Ivy
/**
* Copyright 2018 The Feign 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 reactivefeign.methodhandler;
import feign.CollectionFormat;
import feign.MethodMetadata;
import feign.Param;
import feign.QueryMapEncoder;
import feign.RequestTemplate;
import feign.Target;
import feign.querymap.FieldQueryMapEncoder;
import feign.template.UriUtils;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import reactivefeign.client.ReactiveHttpClient;
import reactivefeign.client.ReactiveHttpRequest;
import reactivefeign.publisher.PublisherHttpClient;
import reactivefeign.utils.ContentType;
import reactivefeign.utils.LinkedCaseInsensitiveMap;
import reactivefeign.utils.Pair;
import reactivefeign.utils.SerializedFormData;
import reactor.core.publisher.Mono;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static feign.Util.checkNotNull;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static reactivefeign.utils.FormUtils.serializeForm;
import static reactivefeign.utils.HttpUtils.CONTENT_TYPE_HEADER;
import static reactivefeign.utils.HttpUtils.FORM_URL_ENCODED;
import static reactivefeign.utils.HttpUtils.MULTIPART_MIME_TYPES;
import static reactivefeign.utils.MultiValueMapUtils.add;
import static reactivefeign.utils.MultiValueMapUtils.addAll;
import static reactivefeign.utils.MultiValueMapUtils.addAllOrdered;
import static reactivefeign.utils.MultiValueMapUtils.addOrdered;
import static reactivefeign.utils.StringUtils.cutPrefix;
import static reactivefeign.utils.StringUtils.cutTail;
/**
* Method handler for asynchronous HTTP requests via {@link PublisherHttpClient}.
*
* Transforms method invocation into request that executed by {@link ReactiveHttpClient}.
*
* @author Sergii Karpenko
*/
public class PublisherClientMethodHandler implements MethodHandler {
public static final Pattern SUBSTITUTION_PATTERN = Pattern.compile("\\{([^}]+)\\}");
private final Target> target;
private final MethodMetadata methodMetadata;
private final PublisherHttpClient publisherClient;
private final Function pathExpander;
private final Map>>> headerExpanders;
private final Map> queriesAll;
private final Map>>> queryExpanders;
private final URI staticUri;
private final QueryMapEncoder queryMapEncoder = new FieldQueryMapEncoder();
private final Optional contentType;
private final boolean isMultipart;
private final boolean isFormUrlEncoded;
public PublisherClientMethodHandler(Target> target,
MethodMetadata methodMetadata,
PublisherHttpClient publisherClient) {
this.target = checkNotNull(target, "target must be not null");
this.methodMetadata = checkNotNull(methodMetadata,
"methodMetadata must be not null");
this.publisherClient = checkNotNull(publisherClient, "client must be not null");
RequestTemplate requestTemplate = methodMetadata.template();
this.pathExpander = buildUrlExpandFunction(requestTemplate, target);
this.headerExpanders = buildExpanders(requestTemplate.headers());
this.queriesAll = new HashMap<>(requestTemplate.queries());
this.contentType = getContentType(requestTemplate.headers());
this.isMultipart = contentType.map(ct -> MULTIPART_MIME_TYPES.contains(ct.getMediaType())).orElse(false);
if(isMultipart && methodMetadata.template().bodyTemplate() != null){
throw new IllegalArgumentException("isMultipart && methodMetadata.template().bodyTemplate() != null");
}
this.isFormUrlEncoded = contentType.map(ct -> ct.getMediaType().equalsIgnoreCase(FORM_URL_ENCODED)).orElse(false);
if(!isMultipart && !isFormUrlEncoded) {
methodMetadata.formParams()
.forEach(param -> add(queriesAll, param, "{" + param + "}"));
}
this.queryExpanders = buildExpanders(queriesAll);
//static template (POST & PUT)
if(pathExpander instanceof StaticExpander
&& queriesAll.isEmpty()
&& methodMetadata.queryMapIndex() == null){
staticUri = URI.create(target.url() + cutTail(requestTemplate.url(), "/"));
} else {
staticUri = null;
}
if(methodMetadata.indexToExpander() == null && methodMetadata.indexToExpanderClass() != null) {
methodMetadata.indexToExpander(instantiateExpanders(methodMetadata.indexToExpanderClass()));
}
}
@Override
public Publisher> invoke(final Object[] argv) {
return publisherClient.executeRequest(buildRequest(argv));
}
protected ReactiveHttpRequest buildRequest(Object[] argv) {
Object[] argsExpanded = expandArguments(argv);
Substitutions substitutions = buildSubstitutions(argsExpanded);
URI uri = buildUri(argsExpanded, substitutions);
Map> headers = headers(argsExpanded, substitutions);
Publisher body = body(argsExpanded, substitutions);
return new ReactiveHttpRequest(methodMetadata, target, uri, headers, body);
}
private URI buildUri(Object[] argv, Substitutions substitutions) {
//static template
if(staticUri != null){
return staticUri;
}
String path = pathExpander.apply(substitutions);
Map> queries = queries(argv, substitutions);
String queryLine = queryLine(queries);
return URI.create(path + queryLine);
}
private Substitutions buildSubstitutions(Object[] argv) {
Map substitutions = methodMetadata.indexToName().entrySet().stream()
.filter(e -> argv[e.getKey()] != null)
.flatMap(e -> e.getValue().stream()
.map(v -> new AbstractMap.SimpleImmutableEntry<>(e.getKey(), v)))
.collect(toMap(Map.Entry::getValue,
entry -> argv[entry.getKey()]));
URI url = methodMetadata.urlIndex() != null ? (URI) argv[methodMetadata.urlIndex()] : null;
return new Substitutions(substitutions, url);
}
private String queryLine(Map> queries) {
if (queries.isEmpty()) {
return "";
}
StringBuilder queryBuilder = new StringBuilder();
CollectionFormat collectionFormat = methodMetadata.template().collectionFormat();
for (Map.Entry> query : queries.entrySet()) {
Collection valuesEncoded = query.getValue().stream()
.map(value -> UriUtils.encode(value, UTF_8))
.collect(toList());
queryBuilder.append('&');
queryBuilder.append(collectionFormat.join(query.getKey(), valuesEncoded, UTF_8));
}
if(queryBuilder.length() > 0) {
queryBuilder.deleteCharAt(0);
return queryBuilder.insert(0, '?').toString();
} else {
return "";
}
}
protected Map> queries(Object[] argv,
Substitutions substitutions) {
Map> queries = new LinkedHashMap<>();
// queries from template
queriesAll.keySet().forEach(queryName -> addAll(queries, queryName,
queryExpanders.getOrDefault(queryName, singletonList(subs -> singletonList(""))).stream()
.map(expander -> expander.apply(substitutions))
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.collect(toList())));
// queries from args
if (methodMetadata.queryMapIndex() != null) {
Object queryMapObject = argv[methodMetadata.queryMapIndex()];
if(queryMapObject != null){
Map queryMap = queryMapObject instanceof Map
? (Map) queryMapObject
: queryMapEncoder.encode(queryMapObject);
queryMap.forEach((key, value) -> {
if (value instanceof Iterable) {
((Iterable>) value).forEach(element -> add(queries, key, element.toString()));
} else if (value != null) {
add(queries, key, value.toString());
}
});
}
}
return queries;
}
protected Map> headers(Object[] argv, Substitutions substitutions) {
Map> headers = new LinkedCaseInsensitiveMap<>();
// headers from template
methodMetadata.template().headers().keySet()
.forEach(headerName -> addAllOrdered(headers, headerName,
headerExpanders.get(headerName).stream()
.map(expander -> expander.apply(substitutions))
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.collect(toList())));
// headers from args
if (methodMetadata.headerMapIndex() != null) {
((Map) argv[methodMetadata.headerMapIndex()])
.forEach((key, value) -> {
if (value instanceof Iterable) {
((Iterable>) value)
.forEach(element -> addOrdered(headers, key, element.toString()));
} else {
addOrdered(headers, key, value.toString());
}
});
}
return headers;
}
protected Publisher body(
Object[] argv,
Substitutions substitutions) {
if(isFormUrlEncoded){
return serializeFormData(argv, substitutions);
}
if (methodMetadata.bodyIndex() != null) {
Object body = argv[methodMetadata.bodyIndex()];
return body(body);
} else if(isMultipart) { //all arguments have Param annotation
return new MultipartMap(collectFormData(substitutions));
} else {
return Mono.empty();
}
}
private SerializedFormData serializeFormData(Object[] argv, Substitutions substitutions) {
Map formData;
if (methodMetadata.bodyIndex() != null) {
Object body = argv[methodMetadata.bodyIndex()];
formData = (Map)body;
} else {
formData = collectFormData(substitutions);
}
return serializeForm(formData, contentType.get().getCharset());
}
private Map> collectFormData(Substitutions substitutions) {
Map> formVariables = new LinkedHashMap<>();
for (Map.Entry entry : substitutions.placeholderToSubstitution.entrySet()) {
if (methodMetadata.formParams().contains(entry.getKey())) {
formVariables.put(entry.getKey(), singletonList(entry.getValue()));
}
}
return formVariables;
}
protected Publisher body(Object body) {
if (body instanceof Publisher) {
return (Publisher) body;
} else {
return Mono.just(body);
}
}
private static Map>>> buildExpanders(
Map> templates) {
Stream> templatesFlattened = templates.entrySet().stream()
.flatMap(e -> e.getValue().stream()
.map(v -> new Pair<>(e.getKey(), v)));
return templatesFlattened.collect(groupingBy(
entry -> entry.left,
mapping(entry -> buildMultiValueExpandFunction(entry.right), toList())));
}
/**
*
* @param template
* @return function that able to map substitutions map to actual value for specified template
*/
private static Function> buildMultiValueExpandFunction(String template) {
Matcher matcher = SUBSTITUTION_PATTERN.matcher(template);
if(matcher.matches()){
String placeholder = matcher.group(1);
return substitutions -> {
Object substitution = substitutions.placeholderToSubstitution.get(placeholder);
if (substitution != null) {
if(substitution instanceof Iterable){
List stringValues = new ArrayList<>();
((Iterable>) substitution).forEach(o -> stringValues.add(o.toString()));
return stringValues;
} else if(substitution instanceof Object[]){
List stringValues = new ArrayList<>(((Object[]) substitution).length);
(asList((Object[])substitution)).forEach(o -> stringValues.add(o.toString()));
return stringValues;
}
else {
return singletonList(substitution.toString());
}
} else {
return null;
}
};
} else {
Function expandFunction = buildExpandFunction(template);
return substitutions -> singletonList(expandFunction.apply(substitutions));
}
}
private static Function buildUrlExpandFunction(
RequestTemplate requestTemplate, Target> target) {
String requestUrl = getRequestUrl(requestTemplate);
if(target instanceof Target.EmptyTarget){
return expandUrlForEmptyTarget(buildExpandFunction(requestUrl));
} else {
String targetUrl = cutTail(target.url(), "/");
return buildExpandFunction(targetUrl+requestUrl);
}
}
private static String getRequestUrl(RequestTemplate requestTemplate) {
String requestUrl = cutTail(requestTemplate.url(), requestTemplate.queryLine());
requestUrl = cutPrefix(requestUrl, "/");
if(!requestUrl.isEmpty()){
requestUrl = "/" + requestUrl;
}
return requestUrl;
}
private static Function expandUrlForEmptyTarget(
Function expandFunction){
return substitutions -> substitutions.url.toString() + expandFunction.apply(substitutions);
}
/**
*
* @param template
* @return function that able to map substitutions map to actual value for specified template
*/
private static Function buildExpandFunction(String template) {
List> chunks = new ArrayList<>();
Matcher matcher = SUBSTITUTION_PATTERN.matcher(template);
int previousMatchEnd = 0;
while (matcher.find()) {
String textChunk = template.substring(previousMatchEnd, matcher.start());
if (textChunk.length() > 0) {
chunks.add(data -> textChunk);
}
String placeholder = matcher.group(1);
chunks.add(data -> {
Object substitution = data.placeholderToSubstitution.get(placeholder);
if (substitution != null) {
return UriUtils.encode(substitution.toString(), UTF_8);
} else {
throw new IllegalArgumentException("No substitution in url for:"+placeholder);
}
});
previousMatchEnd = matcher.end();
}
//no substitutions in template
if(previousMatchEnd == 0){
return new StaticExpander(template);
}
String textChunk = template.substring(previousMatchEnd);
if (textChunk.length() > 0) {
chunks.add(data -> textChunk);
}
return substitutions -> chunks.stream().map(chunk -> chunk.apply(substitutions))
.collect(Collectors.joining());
}
private static class StaticExpander implements Function{
private final String staticPath;
private StaticExpander(String staticPath) {
this.staticPath = staticPath;
}
@Override
public String apply(Substitutions substitutions) {
return staticPath;
}
}
private static class Substitutions {
private final URI url;
private final Map placeholderToSubstitution;
private Substitutions(Map placeholderToSubstitution, URI url) {
this.url = url;
this.placeholderToSubstitution = placeholderToSubstitution;
}
}
private Map instantiateExpanders(Map> indexToExpanderClass) {
Map indexToExpanderMap = new HashMap<>(indexToExpanderClass.size());
indexToExpanderClass.forEach((index, expanderClass) -> {
try {
indexToExpanderMap.put(index, expanderClass.getDeclaredConstructor().newInstance());
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
throw new IllegalStateException(e);
}
});
return indexToExpanderMap;
}
private Object[] expandArguments(Object[] args) {
if(args == null || args.length == 0){
return args;
}
Map integerExpanderMap = methodMetadata.indexToExpander();
if(integerExpanderMap == null || integerExpanderMap.size() == 0){
return args;
}
Object[] argsExpanded = new Object[args.length];
for(int i = 0, n = args.length; i < n; i++){
Param.Expander expander = integerExpanderMap.get(i);
if(expander != null){
argsExpanded[i] = expandElements(expander, args[i]);
} else {
argsExpanded[i] = args[i];
}
}
return argsExpanded;
}
private Object expandElements(Param.Expander expander, Object value) {
if (value instanceof Iterable) {
return expandIterable(expander, (Iterable) value);
}
return expander.expand(value);
}
private List expandIterable(Param.Expander expander, Iterable value) {
List values = new ArrayList();
for (Object element : value) {
if (element != null) {
values.add(expander.expand(element));
}
}
return values;
}
public static class MultipartMap implements Publisher {
private final Map> map;
public MultipartMap(Map> map) {
this.map = map;
}
public Map> getMap() {
return map;
}
@Override
public void subscribe(Subscriber super Object> s) {
throw new UnsupportedOperationException();
}
}
public Optional getContentType(Map> headers){
return headers.entrySet().stream()
.filter(entry -> entry.getKey().equalsIgnoreCase(CONTENT_TYPE_HEADER))
.map(entry -> ContentType.parse(entry.getValue().iterator().next().toLowerCase()))
.findFirst();
}
}