8000 Added ability to expand and encode variables in body annotation and expand request line variables that are not already key pairs by sjhorn · Pull Request #107 · OpenFeign/feign · GitHub
[go: up one dir, main page]

Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/src/main/java/feign/Contract.java
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[
String varName = '{' + name + '}';
if (data.template().url().indexOf(varName) == -1 &&
!searchMapValues(data.template().queries(), varName) &&
!data.template().queries().containsKey(varName) &&
!searchMapValues(data.template().headers(), varName)) {
data.formParams().add(name);
}
Expand Down
34 changes: 34 additions & 0 deletions core/src/main/java/feign/ReflectiveFeign.java
8000
Original file line numberDiff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.io.UnsupportedEncodingException;

import static feign.Util.checkArgument;
import static feign.Util.checkNotNull;
Expand Down Expand Up @@ -147,6 +148,8 @@ public Map<String, MethodHandler> apply(Target key) {
BuildTemplateByResolvingArgs buildTemplate;
if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) {
buildTemplate = new BuildFormEncodedTemplateFromArgs(md, encoder);
} else if (!md.formParams().isEmpty() && md.template().bodyTemplate() != null && !(encoder instanceof Encoder.Default)) {
buildTemplate = new BuildFormTemplateByResolvingAndEncodingArgs(md, encoder);
} else if (md.bodyIndex() != null) {
buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder);
} else {
Expand Down Expand Up @@ -188,6 +191,37 @@ protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map<St
}
}

private static class BuildFormTemplateByResolvingAndEncodingArgs extends BuildTemplateByResolvingArgs {
private final Encoder encoder;

private BuildFormTemplateByResolvingAndEncodingArgs(MethodMetadata metadata, Encoder encoder) {
super(metadata);
this.encoder = encoder;
}

@Override
protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map<String, Object> variables) {
Map<String, Object> formVariables = new LinkedHashMap<String, Object>();
RequestTemplate tempTemplate = new RequestTemplate();
for (Entry<String, Object> entry : variables.entrySet()) {
if (metadata.formParams().contains(entry.getKey()) && !(entry.getValue() instanceof String) ) {
try {
encoder.encode(entry.getValue(), tempTemplate);
variables.put(entry.getKey(), new String(tempTemplate.body(), "UTF-8"));
} catch (EncodeException e) {
throw e;
} catch (RuntimeException e) {
throw new EncodeException(e.getMessage(), e);
} catch (UnsupportedEncodingException uee) {
throw new EncodeException(uee.getMessage(), uee);
}
}
}

return mutable.resolve(variables);
}
}

private static class BuildFormEncodedTemplateFromArgs extends BuildTemplateByResolvingArgs {
private final Encoder encoder;

Expand Down
41 changes: 29 additions & 12 deletions core/src/main/java/feign/RequestTemplate.java
Original file line number Diff line number Diff line change
Expand Up @@ -469,14 +469,15 @@ private StringBuilder pullAnyQueriesOutOfUrl(StringBuilder url) {
//a valid queryLine()
for(String key : firstQueries.keySet()) {
Collection<String> values = firstQueries.get(key);
if(allValuesAreNull(values)) {
if (Util.isVariable(key)) {
queries.put(key,values);
} else if(allValuesAreNull(values)) {
//Queryies where all values are null will
//be ignored by the query(key, value)-method
//So we manually avoid this case here, to ensure that
//we still fulfill the contract (ex. parameters without values)
queries.put(urlEncode(key), values);
}
else {
} else {
query(key, values);
}

Expand Down Expand Up @@ -547,12 +548,23 @@ public void replaceQueryValues(Map<String, ?> unencoded) {
Iterator<Entry<String, Collection<String>>> iterator = queries.entrySet().iterator();
while (iterator.hasNext()) {
Entry<String, Collection<String>> entry = iterator.next();
if (entry.getValue() == null) {
Collection<String> entryValue = entry.getValue();
String entryKey = entry.getKey();
if (entryKey == null) {
continue;
}
if (Util.isVariable(entryKey)) {
Object variableValue = unencoded.get(entryKey.substring(1, entryKey.length() - 1));
entry.setValue(Arrays.asList(Util.variableToQueryString(variableValue)));
continue;
}
if (entryValue == null) {
continue;
}
Collection<String> values = new ArrayList<String>();
for (String value : entry.getValue()) {
if (value.indexOf('{') == 0 && value.indexOf('}') == value.length() - 1) {
for (String value : entryValue) {
if (value == null) continue;
if (Util.isVariable(value)) {
Object variableValue = unencoded.get(value.substring(1, value.length() - 1));
// only add non-null expressions
if (variableValue == null) {
Expand Down Expand Up @@ -582,13 +594,18 @@ public String queryLine() {
return "";
StringBuilder queryBuilder = new StringBuilder();
for (String field : queries.keySet()) {
for (String value : valuesOrEmpty(queries, field)) {
if(Util.isVariable(field)) {
queryBuilder.append('&');
queryBuilder.append(field);
if (value != null) {
queryBuilder.append('=');
if (!value.isEmpty())
queryBuilder.append(value);
queryBuilder.append(queries.get(field).iterator().next());
} else {
for (String value : valuesOrEmpty(queries, field)) {
queryBuilder.append('&');
queryBuilder.append(field);
if (value != null) {
queryBuilder.append('=');
if (!value.isEmpty())
queryBuilder.append(value);
}
}
}
}
Expand Down
72 changes: 72 additions & 0 deletions core/src/main/java/feign/Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,36 @@
*/
package feign;

import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.net.URLEncoder;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import static feign.Util.UTF_8;
import static java.lang.String.format;

/**
Expand Down Expand Up @@ -243,4 +254,65 @@ static String decodeOrDefault(byte[] data, Charset charset, String defaultValue)
return defaultValue;
}
}

public static String variableToQueryString(Object variableValue) {
if (variableValue == null) return null;
List<String> KVPairs = new ArrayList<String>();
if (variableValue instanceof Map) {
Iterator<?> entries = ((Map<?, ?>) variableValue).entrySet().iterator();
while (entries.hasNext()) {
Entry<?, ?> entry = (Entry<?, ?>) entries.next();
Object itemKey = entry.getKey();
Object value = entry.getValue();
if (itemKey != null && itemKey instanceof String && value != null) {
KVPairs.add(encodePair(itemKey, value));
}
}
} else if (variableValue instanceof Object) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm this looks kindof meh. Seems we should not do something as magical as this, and instead use the type adapter system. feign tries really hard to avoid reflection.

try {
for(PropertyDescriptor propertyDescriptor : Introspector.getBeanInfo(variableValue.getClass()).getPropertyDescriptors()) {
Method reader = propertyDescriptor.getReadMethod();
String methodName = propertyDescriptor.getDisplayName();
if (reader != null && methodName.indexOf("class") != 0 && methodName.indexOf("metaClass") != 0) {
KVPairs.add(encodePair(methodName, reader.invoke(variableValue)));
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
if (KVPairs.size() > 0) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < KVPairs.size(); i++) {
sb.append("&").append(KVPairs.get(i));
}
sb.deleteCharAt(0);
return sb.toString();
}
return null;
}

public static String encodePair(Object key, Object value) {
StringBuilder sb = new StringBuilder();
return sb
.append(urlEncode(key))
.append("=")
.append(urlEncode(value))
.toString();
}

public static String urlEncode(Object arg) {
try {
return URLEncoder.encode(String.valueOf(arg), UTF_8.name());
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}

public static boolean isVariable(String item) {
if (item == null || item.length() == 0) {
return false;
}
return (item.indexOf('{') == 0 && item.indexOf('}') == item.length() - 1);
}
}
43 changes: 43 additions & 0 deletions core/src/test/java/feign/FeignTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,49 @@ public void postBodyParam() throws IOException, InterruptedException {
server.shutdown();
}
}

class Operation {
public String param1;
public String param2;
public Operation(String param1, String param2) {
this.param1 = param1;
this.param2 = param2;
}
}
interface ObjectBodyParamInterface {
@RequestLine("POST /sdpapi/request")
@Body("OPERATION_NAME=GET_REQUESTS&TECHNICIAN_KEY={technicalKey}&INPUT_DATA={inputData}")
public String getRequests(
@Named("technicalKey") String technicalKey,
@Named("inputData") Operation operation
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ex. if this weren't @Named, a custom encoder could be used to add the &INPUT_DATA=splat(operation)

);
}

@Test
public void postBodyEncodedBodyParam() throws Exception {
final MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setBody("foo"));
server.play();
ObjectBodyParamInterface api = Feign.builder()
.encoder(new Encoder() {
public void encode(Object object, RequestTemplate template)
throws feign.codec.EncodeException {
if(object instanceof Operation) {
Operation op = (Operation) object;
template.body("<param1>"+op.param1+"</param1>"
+"<param2>"+op.param2+"</param2>");
} else {
template.body(object.toString());
}
}
})
.target(ObjectBodyParamInterface.class, "http://localhost:"+ server.getPort());
api.getRequests("Test", new Operation("param1", "param2"));
RecordedRequest request = server.takeRequest();

assertEquals(new String(request.getBody(), UTF_8),
"OPERATION_NAME=GET_REQUESTS&TECHNICIAN_KEY=Test&INPUT_DATA=<param1>param1</param1><param2>param2</param2>");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ps I sincerely pity you for having to deal with an api that encodes xml as a query param :P

}

@Test
public void postGZIPEncodedBodyParam() throws IOException, InterruptedException {
Expand Down
42 changes: 42 additions & 0 deletions core/src/test/java/feign/RequestTemplateTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -201,4 +201,46 @@ ImmutableListMultimap.<String, String> builder()
assertEquals(template.toString(), ""//
+ "GET /domains/1001/records HTTP/1.1\n");
}

@Test public void expandMapOfKeyPairForQuery() throws Exception {
RequestTemplate template = new RequestTemplate().method("POST")
.append("/1/cards")//
.query("key", "{key}")//
.query("token", "{token}")
.query("name", "{name}")
.query("idList", "{idList}")
.query("{fields}", ImmutableList.of(""));

ImmutableMap<String,Object> fields = ImmutableMap.of(
"label", (Object)"blue",
"description", (Object)"nice");

template = template.resolve(ImmutableMap.<String, Object>builder()
.put("key", "9f867f42asdfasdfa406981c0468b0134")
.put("token", "asdfasdf87df285c943b7627basdfasdfc9df8cde08a81f380935")
.put("name", "CardName")
.put("idList", Arrays.asList("537be504aaf676a1adee4871"))
.put("fields", fields)
.build()
);

assertEquals(template.toString(), ""
+ "POST /1/cards?key=9f867f42asdfasdfa406981c0468b0134&"
+ "token=asdfasdf87df285c943b7627basdfasdfc9df8cde08a81f380935&"
+ "name=CardName&"
+ "idList=537be504aaf676a1adee4871&"
+ "label=blue&"
+ "description=nice HTTP/1.1\n");
}

@Test public void fieldsInRequestLine() throws Exception {
RequestTemplate template = new RequestTemplate()
.method("PUT")
.append("/1/cards/{cardIdOrShortLink}?key={key}&token={token}&{fields}");

assertEquals(template.queries().size(), 3);
assertEquals(template.queries().containsKey("{fields}"), true);
}


}
Loading
0