Skip to content

Commit 8780a0c

Browse files
author
Adrian Cole
committed
Merge pull request OpenFeign#20 from Netflix/websocket-decoder
Normalized to Decoder.TextStream and Encoder.Text; Provides.Type.SET binding
2 parents 930edd0 + 9fd513d commit 8780a0c

32 files changed

Lines changed: 1357 additions & 516 deletions

CHANGES.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
### Version 3.0
22
* Wire is now Logger, with configurable Logger.Level.
3-
* decoupled ErrorDecoder from fallback handling
4-
* Decoders can throw checked exceptions, but needn't declare Throwable
5-
* Decoders no longer read methodKey
3+
* changed codec to be similar to [WebSocket JSR 356](http://docs.oracle.com/javaee/7/api/javax/websocket/package-summary.html)
4+
* Decoder is now `Decoder.TextStream<T>`
5+
* BodyEncoder is now `Encoder.Text<T>`
6+
* FormEncoder is now `Encoder.Text<Map<String, ?>>`
7+
* Encoder and Decoders are specified via `Provides.Type.SET` binding.
8+
* Default Encoder and Form Encoder is `Encoder.Text<Object>`
9+
* Default Decoder is `Decoder.TextStream<Object>`
10+
* ErrorDecoder now returns Exception, not fallback.
11+
* There can only be one `ErrorDecoder` and `Request.Options` binding now.
612

713
### Version 2.0.0
814
* removes guava and jax-rs dependencies

README.md

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Feign makes writing java http clients easier
2-
Feign is a java to http client binder inspired by [Dagger](https://github.com/square/dagger), [Retrofit](https://github.com/square/retrofit), [jclouds](https://github.com/jclouds/jclouds), and [JAXRS-2.0](https://jax-rs-spec.java.net/nonav/2.0/apidocs/index.html). Feign's first goal was reducing the complexity of binding [Denominator](https://github.com/Netflix/Denominator) uniformly to http apis regardless of [restfulness](http://www.slideshare.net/adrianfcole/99problems).
2+
Feign is a java to http client binder inspired by [Dagger](https://github.com/square/dagger), [Retrofit](https://github.com/square/retrofit), [JAXRS-2.0](https://jax-rs-spec.java.net/nonav/2.0/apidocs/index.html), and [WebSockets](http://www.oracle.com/technetwork/articles/java/jsr356-1937161.html). Feign's first goal was reducing the complexity of binding [Denominator](https://github.com/Netflix/Denominator) uniformly to http apis regardless of [restfulness](http://www.slideshare.net/adrianfcole/99problems).
33

44
### Why Feign and not X?
55

@@ -35,26 +35,39 @@ public static void main(String... args) {
3535
}
3636
```
3737
### Decoders
38-
The last argument to `Feign.create` specifies how to decode the responses. You can plug-in your favorite library, such as gson, or use builtin RegEx Pattern decoders. Here's how the Gson module looks.
38+
The last argument to `Feign.create` specifies how to decode the responses, modeled in Dagger. Here's how it looks to wire in a default gson decoder:
3939

4040
```java
4141
@Module(overrides = true, library = true)
4242
static class GsonModule {
43-
@Provides @Singleton Map<String, Decoder> decoders() {
44-
return ImmutableMap.of("GitHub", gsonDecoder);
43+
@Provides(type = SET) Decoder decoder() {
44+
return new Decoder.TextStream<Object>() {
45+
Gson gson = new Gson();
46+
47+
@Override public Object decode(Reader reader, Type type) throws IOException {
48+
try {
49+
return gson.fromJson(reader, type);
50+
} catch (JsonIOException e) {
51+
if (e.getCause() != null && e.getCause() instanceof IOException) {
52+
throw IOException.class.cast(e.getCause());
53+
}
54+
throw e;
55+
}
56+
}
57+
};
4558
}
46-
47-
final Decoder gsonDecoder = new Decoder() {
48-
Gson gson = new Gson();
49-
50-
@Override public Object decode(String methodKey, Reader reader, Type type) {
51-
return gson.fromJson(reader, type);
52-
}
53-
};
5459
}
5560
```
5661
Feign doesn't offer a built-in json decoder as you can see above it is very few lines of code to wire yours in. If you are a jackson user, you'd probably thank us for not dragging in a dependency you don't use.
5762

63+
#### Type-specific Decoders
64+
The generic parameter of `Decoder.TextStream<T>` designates which The type parameter is either a concrete type, or `Object`, if your decoder can handle multiple types. To add a type-specific decoder, ensure your type parameter is correct. Here's an example of an xml decoder that will only apply to methods that return `ZoneList`.
65+
66+
```
67+
@Provides(type = SET) Decoder zoneListDecoder(Provider<ListHostedZonesResponseHandler> handlers) {
68+
return new SAXDecoder<ZoneList>(handlers){};
69+
}
70+
```
5871
### Multiple Interfaces
5972
Feign can produce multiple api interfaces. These are defined as `Target<T>` (default `HardCodedTarget<T>`), which allow for dynamic discovery and decoration of requests prior to execution.
6073

@@ -89,10 +102,14 @@ MyService api = Feign.create(MyService.class, "https://myAppProd", new RibbonMod
89102
#### Dagger
90103
Feign can be directly wired into Dagger which keeps things at compile time and Android friendly. As opposed to exposing builders for config, Feign intends users to embed their config in Dagger.
91104

92-
Almost all configuration of Feign is represented as Map bindings, where the key is either the simple name (ex. `GitHub`) or the method (ex. `GitHub#contributors()`) in javadoc link format. For example, the following routes all decoding to gson:
105+
Where possible, Feign configuration uses normal Dagger conventions. For example, `Decoder` bindings are of `Provider.Type.SET`, meaning you can make multiple bindings for all the different types you return. Here's an example of multiple decoder bindings.
93106
```java
94-
@Provides @Singleton Map<String, Decoder> decoders() {
95-
return ImmutableMap.of("GitHub", gsonDecoder);
107+
@Provides(type = SET) Decoder recordListDecoder(Provider<RecordListHandler> handlers) {
108+
return new SAXDecoder<List<Record>>(handlers){};
109+
}
110+
111+
@Provides(type = SET) Decoder directionalRecordListDecoder(Provider<DirectionalRecordListHandler> handlers) {
112+
return new SAXDecoder<List<DirectionalRecord>>(handlers){};
96113
}
97114
```
98115
#### Logging
@@ -117,8 +134,8 @@ Here's how our IAM example grabs only one xml element from a response.
117134
```java
118135
@Module(overrides = true, library = true)
119136
static class IAMModule {
120-
@Provides @Singleton Map<String, Decoder> decoders() {
121-
return ImmutableMap.of("IAM#arn()", Decoders.firstGroup("<Arn>([\\S&&[^<]]+)</Arn>"));
137+
@Provides(type = SET) Decoder arnDecoder() {
138+
return Decoders.firstGroup("<Arn>([\\S&&[^<]]+)</Arn>");
122139
}
123140
}
124141
```

feign-core/src/main/java/feign/Contract.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ public MethodMetadata parseAndValidatateMetadata(Method method) {
7373
checkState(data.formParams().isEmpty(), "Body parameters cannot be used with @FormParam parameters.");
7474
checkState(data.bodyIndex() == null, "Method has too many Body parameters: %s", method);
7575
data.bodyIndex(i);
76+
data.bodyType(method.getGenericParameterTypes()[i]);
7677
}
7778
}
7879
return data;
@@ -112,7 +113,7 @@ protected void nameParam(MethodMetadata data, String name, int i) {
112113
data.indexToName().put(i, names);
113114
}
114115

115-
static class DefaultContract extends Contract {
116+
static class Default extends Contract {
116117

117118
@Override
118119
protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) {

feign-core/src/main/java/feign/Feign.java

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,21 @@
1515
*/
1616
package feign;
1717

18-
import java.lang.reflect.Method;
19-
import java.util.ArrayList;
20-
import java.util.Collections;
21-
import java.util.List;
22-
import java.util.Map;
23-
24-
import javax.net.ssl.SSLSocketFactory;
25-
2618
import dagger.ObjectGraph;
2719
import dagger.Provides;
20+
import feign.Logger.NoOpLogger;
2821
import feign.Request.Options;
2922
import feign.Target.HardCodedTarget;
30-
import feign.Logger.NoOpLogger;
31-
import feign.codec.BodyEncoder;
3223
import feign.codec.Decoder;
24+
import feign.codec.Encoder;
3325
import feign.codec.ErrorDecoder;
34-
import feign.codec.FormEncoder;
26+
27+
import javax.net.ssl.SSLSocketFactory;
28+
import java.lang.reflect.Method;
29+
import java.util.ArrayList;
30+
import java.util.Collections;
31+
import java.util.List;
32+
import java.util.Set;
3533

3634
/**
3735
* Feign's purpose is to ease development against http apis that feign
@@ -78,16 +76,16 @@ public static ObjectGraph createObjectGraph(Object... modules) {
7876
return ObjectGraph.create(modulesForGraph(modules).toArray());
7977
}
8078

79+
@SuppressWarnings("rawtypes")
8180
@dagger.Module(complete = false, injects = Feign.class, library = true)
8281
public static class Defaults {
8382

84-
@Provides
85-
Logger.Level logLevel() {
83+
@Provides Logger.Level logLevel() {
8684
return Logger.Level.NONE;
8785
}
8886

8987
@Provides Contract contract() {
90-
return new Contract.DefaultContract();
88+
return new Contract.Default();
9189
}
9290

9391
@Provides SSLSocketFactory sslSocketFactory() {
@@ -106,24 +104,20 @@ Logger.Level logLevel() {
106104
return new NoOpLogger();
107105
}
108106

109-
@Provides Map<String, Options> noOptions() {
110-
return Collections.emptyMap();
111-
}
112-
113-
@Provides Map<String, BodyEncoder> noBodyEncoders() {
114-
return Collections.emptyMap();
107+
@Provides ErrorDecoder errorDecoder() {
108+
return new ErrorDecoder.Default();
115109
}
116110

117-
@Provides Map<String, FormEncoder> noFormEncoders() {
118-
return Collections.emptyMap();
111+
@Provides Options options() {
112+
return new Options();
119113
}
120114

121-
@Provides Map<String, Decoder> noDecoders() {
122-
return Collections.emptyMap();
115+
@Provides Set<Encoder> noEncoders() {
116+
return Collections.emptySet();
123117
}
124118

125-
@Provides Map<String, ErrorDecoder> noErrorDecoders() {
126-
return Collections.emptyMap();
119+
@Provides Set<Decoder> noDecoders() {
120+
return Collections.emptySet();
127121
}
128122
}
129123

feign-core/src/main/java/feign/FeignException.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515
*/
1616
package feign;
1717

18+
import static java.lang.String.format;
19+
1820
import java.io.IOException;
1921

2022
import feign.codec.StringDecoder;
2123

22-
import static java.lang.String.format;
23-
2424
/**
2525
* Origin exception type for all Http Apis.
2626
*/
@@ -34,8 +34,8 @@ static FeignException errorReading(Request request, Response response, IOExcepti
3434
public static FeignException errorStatus(String methodKey, Response response) {
3535
String message = format("status %s reading %s", response.status(), methodKey);
3636
try {
37-
Object body = toString.decode(response, String.class);
38-
if (body != null) {
37+
if (response.body() != null) {
38+
String body = toString.decode(response.body().asReader(), String.class);
3939
response = Response.create(response.status(), response.reason(), response.headers(), body.toString());
4040
message += "; content:\n" + body;
4141
}

feign-core/src/main/java/feign/Logger.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import java.io.BufferedReader;
1919
import java.io.IOException;
2020
import java.io.Reader;
21-
import java.text.SimpleDateFormat;
2221
import java.util.logging.FileHandler;
2322
import java.util.logging.LogRecord;
2423
import java.util.logging.SimpleFormatter;
@@ -90,18 +89,16 @@ Response logAndRebufferResponse(Target<?> target, Level logLevel, Response respo
9089
}
9190

9291
/**
93-
* helper that configures jul to sanely log messages.
92+
* helper that configures jul to sanely log messages at FINE level without additional formatting.
9493
*/
9594
public JavaLogger appendToFile(String logfile) {
96-
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
9795
logger.setLevel(java.util.logging.Level.FINE);
9896
try {
9997
FileHandler handler = new FileHandler(logfile, true);
10098
handler.setFormatter(new SimpleFormatter() {
10199
@Override
102100
public String format(LogRecord record) {
103-
String timestamp = sdf.format(new java.util.Date(record.getMillis())); // NOPMD
104-
return String.format("%s %s%n", timestamp, record.getMessage()); // NOPMD
101+
return String.format("%s%n", record.getMessage()); // NOPMD
105102
}
106103
});
107104
logger.addHandler(handler);

feign-core/src/main/java/feign/MethodHandler.java

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,16 @@
1515
*/
1616
package feign;
1717

18-
import java.io.IOException;
19-
import java.util.concurrent.TimeUnit;
20-
21-
import javax.inject.Inject;
22-
import javax.inject.Provider;
23-
2418
import feign.Request.Options;
19+
import feign.codec.DecodeException;
2520
import feign.codec.Decoder;
2621
import feign.codec.ErrorDecoder;
2722

23+
import javax.inject.Inject;
24+
import javax.inject.Provider;
25+
import java.io.IOException;
26+
import java.util.concurrent.TimeUnit;
27+
2828
import static feign.FeignException.errorExecuting;
2929
import static feign.FeignException.errorReading;
3030
import static feign.Util.checkNotNull;
@@ -54,30 +54,36 @@ static class Factory {
5454
}
5555

5656
public MethodHandler create(Target<?> target, MethodMetadata md, BuildTemplateFromArgs buildTemplateFromArgs,
57-
Options options, Decoder decoder, ErrorDecoder errorDecoder) {
58-
return new SynchronousMethodHandler(target, client, retryer, logger, logLevel, md, buildTemplateFromArgs,
59-
options, decoder, errorDecoder);
57+
Options options, Decoder.TextStream<?> decoder, ErrorDecoder errorDecoder) {
58+
return new SynchronousMethodHandler(target, client, retryer, logger, logLevel, md, buildTemplateFromArgs, options,
59+
decoder, errorDecoder);
6060
}
6161
}
6262

6363
static final class SynchronousMethodHandler extends MethodHandler {
64-
private final Decoder decoder;
64+
private final Decoder.TextStream<?> decoder;
6565

6666
private SynchronousMethodHandler(Target<?> target, Client client, Provider<Retryer> retryer, Logger logger,
6767
Logger.Level logLevel, MethodMetadata metadata,
68-
BuildTemplateFromArgs buildTemplateFromArgs, Options options, Decoder decoder,
69-
ErrorDecoder errorDecoder) {
68+
BuildTemplateFromArgs buildTemplateFromArgs, Options options,
69+
Decoder.TextStream<?> decoder, ErrorDecoder errorDecoder) {
7070
super(target, client, retryer, logger, logLevel, metadata, buildTemplateFromArgs, options, errorDecoder);
7171
this.decoder = checkNotNull(decoder, "decoder for %s", target);
7272
}
7373

7474
@Override protected Object decode(Object[] argv, Response response) throws Throwable {
7575
if (metadata.returnType().equals(Response.class)) {
7676
return response;
77-
} else if (metadata.returnType() == void.class) {
77+
} else if (metadata.returnType() == void.class || response.body() == null) {
7878
return null;
7979
}
80-
return decoder.decode(response, metadata.returnType());
80+
try {
81+
return decoder.decode(response.body().asReader(), metadata.returnType());
82+
} catch (FeignException e) {
83+
throw e;
84+
} catch (RuntimeException e) {
85+
throw new DecodeException(e.getMessage(), e);
86+
}
8187
}
8288
}
8389

feign-core/src/main/java/feign/MethodMetadata.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public final class MethodMetadata implements Serializable {
3131
private transient Type returnType;
3232
private Integer urlIndex;
3333
private Integer bodyIndex;
34+
private transient Type bodyType;
3435
private RequestTemplate template = new RequestTemplate();
3536
private List<String> formParams = new ArrayList<String>();
3637
private Map<Integer, Collection<String>> indexToName = new LinkedHashMap<Integer, Collection<String>>();
@@ -74,6 +75,15 @@ MethodMetadata bodyIndex(Integer bodyIndex) {
7475
return this;
7576
}
7677

78+
public Type bodyType() {
79+
return bodyType;
80+
}
81+
82+
MethodMetadata bodyType(Type bodyType) {
83+
this.bodyType = bodyType;
84+
return this;
85+
}
86+
7787
public RequestTemplate template() {
7888
return template;
7989
}

0 commit comments

Comments
 (0)