-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Expand file tree
/
Copy pathMultiEncoder.java
More file actions
163 lines (148 loc) · 5.56 KB
/
Copy pathMultiEncoder.java
File metadata and controls
163 lines (148 loc) · 5.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
/*
* Copyright © 2012 The Feign Authors (feign@commonhaus.dev)
*
* 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 feign.codec;
import feign.Experimental;
import feign.RequestTemplate;
import feign.Util;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* An {@link Encoder} that hands each request to the first encoder that accepts it.
*
* <p>Encoders come from two places. An encoder that implements {@link PredicatedEncoder} declares
* its own applicability and can simply be added; any other encoder is paired with an {@link
* EncoderPredicate} at the call site:
*
* <pre>
* Feign.builder()
* .encoder(
* MultiEncoder.builder()
* .add(new JacksonEncoder())
* .add(EncoderPredicate.xmlContentType(), new JAXBEncoder())
* .add((object, bodyType, template) -> bodyType == byte[].class, new BinaryEncoder())
* .add(EncoderPredicate.any(), new DefaultEncoder())
* .build());
* </pre>
*
* <p>Encoders are consulted in the order they were added, so the narrowest one comes first. There
* is no implicit fallback: a request no encoder accepts fails with an {@link EncodeException}
* naming what was tried. Add an encoder guarded by {@link EncoderPredicate#any()} last to act as a
* default, as above.
*
* @see PredicatedEncoder
* @see EncoderPredicate
*/
@Experimental
public class MultiEncoder implements Encoder {
private final List<PredicatedEncoder> encoders;
private MultiEncoder(List<PredicatedEncoder> encoders) {
this.encoders = Collections.unmodifiableList(new ArrayList<>(encoders));
}
/** Starts building a multi-encoder. */
public static Builder builder() {
return new Builder();
}
/**
* Encodes using the first encoder that accepts the request.
*
* @param object {@inheritDoc}
* @param bodyType {@inheritDoc}
* @param template {@inheritDoc}
* @throws EncodeException when no encoder accepts the request, or the chosen one fails
*/
@Override
public void encode(Object object, Type bodyType, RequestTemplate template)
throws EncodeException {
for (PredicatedEncoder encoder : encoders) {
if (encoder.canEncode(object, bodyType, template)) {
encoder.encode(object, bodyType, template);
return;
}
}
throw new EncodeException(unableToEncode(bodyType, template));
}
private String unableToEncode(Type bodyType, RequestTemplate template) {
StringBuilder message =
new StringBuilder("Unable to encode ")
.append(bodyType == null ? "request body" : bodyType.getTypeName())
.append(" (Content-Type: ")
.append(contentTypes(template))
.append(')');
if (template.method() != null) {
message.append(" for ").append(template.method()).append(' ').append(template.path());
}
if (encoders.isEmpty()) {
return message.append(". No encoders were configured.").toString();
}
message.append(". Encoders tried, in order:");
for (PredicatedEncoder encoder : encoders) {
message.append("\n - ").append(PairedEncoder.describe(encoder));
}
return message
.append("\nAdd an encoder guarded by EncoderPredicate.any() last to act as a default.")
.toString();
}
private static String contentTypes(RequestTemplate template) {
String contentTypes =
template.headers().entrySet().stream()
.filter(header -> Util.CONTENT_TYPE.equalsIgnoreCase(header.getKey()))
.map(Map.Entry::getValue)
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.collect(Collectors.joining(", "));
return contentTypes.isEmpty() ? "not set" : contentTypes;
}
@Override
public String toString() {
return "MultiEncoder"
+ encoders.stream().map(PairedEncoder::describe).collect(Collectors.toList());
}
/** Collects the encoders of a {@link MultiEncoder}. */
@Experimental
public static final class Builder {
private final List<PredicatedEncoder> encoders = new ArrayList<>();
private Builder() {}
/**
* Adds an encoder that declares its own applicability.
*
* @param encoder the encoder, consulted via {@link PredicatedEncoder#canEncode}
*/
public Builder add(PredicatedEncoder encoder) {
encoders.add(Objects.requireNonNull(encoder, "encoder cannot be null"));
return this;
}
/**
* Adds any encoder, guarded by the given predicate. Use this for encoders that do not implement
* {@link PredicatedEncoder}, including ones you do not control.
*
* @param predicate decides whether the encoder handles a request
* @param encoder the encoder to delegate to
*/
public Builder add(EncoderPredicate predicate, Encoder encoder) {
return add(PredicatedEncoder.of(predicate, encoder));
}
/** Builds the multi-encoder. */
public MultiEncoder build() {
return new MultiEncoder(encoders);
}
}
}