Skip to content

Commit 0ac4f9a

Browse files
author
adriancole
committed
SaxDecoder now decodes multiple types.
1 parent 449aea5 commit 0ac4f9a

4 files changed

Lines changed: 244 additions & 25 deletions

File tree

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
### Version 5.0
22
* Remove support for Observable methods.
3+
* SaxDecoder now decodes multiple types.
34

45
### Version 4.4.1
56
* Fix NullPointerException on calling equals and hashCode.

core/src/main/java/feign/codec/SAXDecoder.java

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,52 @@
2525
import java.io.IOException;
2626
import java.io.Reader;
2727
import java.lang.reflect.Type;
28+
import java.util.LinkedHashMap;
29+
import java.util.Map;
2830

2931
import static feign.Util.checkNotNull;
3032
import static feign.Util.checkState;
33+
import static feign.Util.resolveLastTypeParameter;
34+
35+
/**
36+
* Decodes responses using SAX. Configure using the {@link SAXDecoder.Builder
37+
* builder}.
38+
* <p/>
39+
*
40+
* <pre>
41+
* &#064;Provides(type = SET)
42+
* Decoder saxDecoder(Provider&lt;ContentHandlerForFoo&gt; foo, //
43+
* Provider&lt;ContentHandlerForBar&gt; bar) {
44+
* return SAXDecoder.builder() //
45+
* .addContentHandler(foo) //
46+
* .addContentHandler(bar) //
47+
* .build();
48+
* }
49+
* </pre>
50+
*/
51+
public class SAXDecoder implements Decoder.TextStream<Object> {
52+
53+
public static Builder builder() {
54+
return new Builder();
55+
}
56+
57+
// builder as dagger doesn't support wildcard bindings, map bindings, or set bindings of providers.
58+
public static class Builder {
59+
private final Map<Type, Provider<? extends ContentHandlerWithResult<?>>> handlerProviders =
60+
new LinkedHashMap<Type, Provider<? extends ContentHandlerWithResult<?>>>();
61+
62+
public Builder addContentHandler(Provider<? extends ContentHandlerWithResult<?>> handler) {
63+
Type type = resolveLastTypeParameter(checkNotNull(handler, "handler").getClass(), Provider.class);
64+
type = resolveLastTypeParameter(type, ContentHandlerWithResult.class);
65+
this.handlerProviders.put(type, handler);
66+
return this;
67+
}
68+
69+
public SAXDecoder build() {
70+
return new SAXDecoder(handlerProviders);
71+
}
72+
}
3173

32-
public class SAXDecoder<T> implements Decoder.TextStream<T> {
3374
/* Implementations are not intended to be shared across requests. */
3475
public interface ContentHandlerWithResult<T> extends ContentHandler {
3576
/*
@@ -39,27 +80,17 @@ public interface ContentHandlerWithResult<T> extends ContentHandler {
3980
T result();
4081
}
4182

42-
private final Provider<? extends ContentHandlerWithResult<T>> handlers;
83+
private final Map<Type, Provider<? extends ContentHandlerWithResult<?>>> handlerProviders;
4384

44-
/**
45-
* You must subclass this, in order to prevent type erasure on {@code T}. In
46-
* addition to making a concrete type, you can also use the following form.
47-
* <p/>
48-
* <br>
49-
* <p/>
50-
* <pre>
51-
* new SaxDecoder&lt;Foo&gt;(fooHandlers) {
52-
* }; // note the curly braces ensures no type erasure!
53-
* </pre>
54-
*/
55-
protected SAXDecoder(Provider<? extends ContentHandlerWithResult<T>> handlers) {
56-
this.handlers = checkNotNull(handlers, "handlers");
85+
private SAXDecoder(Map<Type, Provider<? extends ContentHandlerWithResult<?>>> handlerProviders) {
86+
this.handlerProviders = handlerProviders;
5787
}
5888

5989
@Override
60-
public T decode(Reader reader, Type type) throws IOException, DecodeException {
61-
ContentHandlerWithResult<T> handler = handlers.get();
62-
checkState(handler != null, "%s returned null for type %s", this, type);
90+
public Object decode(Reader reader, Type type) throws IOException, DecodeException {
91+
Provider<? extends ContentHandlerWithResult<?>> handlerProvider = handlerProviders.get(type);
92+
checkState(handlerProvider != null, "type %s not in configured handlers %s", type, handlerProviders.keySet());
93+
ContentHandlerWithResult<?> handler = handlerProvider.get();
6394
try {
6495
XMLReader xmlReader = XMLReaderFactory.createXMLReader();
6596
xmlReader.setFeature("http://xml.org/sax/features/namespaces", false);
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
* Copyright 2013 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package feign.codec;
17+
18+
import dagger.ObjectGraph;
19+
import dagger.Provides;
20+
import org.testng.annotations.BeforeClass;
21+
import org.testng.annotations.Test;
22+
import org.xml.sax.helpers.DefaultHandler;
23+
24+
import javax.inject.Inject;
25+
import javax.inject.Provider;
26+
import java.io.IOException;
27+
import java.io.StringReader;
28+
import java.text.ParseException;
29+
import java.util.Set;
30+
31+
import static dagger.Provides.Type.SET;
32+
import static org.testng.Assert.assertEquals;
33+
34+
// unbound wildcards are not currently injectable in dagger.
35+
@SuppressWarnings("rawtypes")
36+
public class SAXDecoderTest {
37+
38+
@dagger.Module(injects = SAXDecoderTest.class)
39+
static class Module {
40+
@Provides(type = SET) Decoder saxDecoder(Provider<NetworkStatusHandler> networkStatus, //
41+
Provider<NetworkStatusStringHandler> networkStatusAsString) {
42+
return SAXDecoder.builder() //
43+
.addContentHandler(networkStatus) //
44+
.addContentHandler(networkStatusAsString) //
45+
.build();
46+
}
47+
}
48+
49+
@Inject Set<Decoder> decoders;
50+
51+
@BeforeClass void inject() {
52+
ObjectGraph.create(new Module()).inject(this);
53+
}
54+
55+
@Test public void parsesConfiguredTypes() throws ParseException, IOException {
56+
Decoder decoder = decoders.iterator().next();
57+
assertEquals(decoder.decode(new StringReader(statusFailed), NetworkStatus.class), NetworkStatus.FAILED);
58+
assertEquals(decoder.decode(new StringReader(statusFailed), String.class), "Failed");
59+
}
60+
61+
@Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp =
62+
"type int not in configured handlers \\[class .*NetworkStatus, class java.lang.String\\]")
63+
public void niceErrorOnUnconfiguredType() throws ParseException, IOException {
64+
Decoder decoder = decoders.iterator().next();
65+
decoder.decode(new StringReader(statusFailed), int.class);
66+
}
67+
68+
static String statusFailed = ""//
69+
+ "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">\n"//
70+
+ " <soap:Body>\n"//
71+
+ " <ns1:getNeustarNetworkStatusResponse xmlns:ns1=\"http://webservice.api.ultra.neustar.com/v01/\">\n"//
72+
+ " <NeustarNetworkStatus xmlns:ns2=\"http://schema.ultraservice.neustar.com/v01/\">Failed</NeustarNetworkStatus>\n"//
73+
+ " </ns1:getNeustarNetworkStatusResponse>\n"//
74+
+ " </soap:Body>\n"//
75+
+ "</soap:Envelope>";
76+
77+
static enum NetworkStatus {
78+
GOOD, FAILED;
79+
}
80+
81+
static class NetworkStatusStringHandler extends DefaultHandler implements
82+
SAXDecoder.ContentHandlerWithResult<String> {
83+
@Inject NetworkStatusStringHandler() {
84+
}
85+
86+
private StringBuilder currentText = new StringBuilder();
87+
88+
private String status;
89+
90+
@Override
91+
public String result() {
92+
return status;
93+
}
94+
95+
@Override
96+
public void endElement(String uri, String name, String qName) {
97+
if (qName.equals("NeustarNetworkStatus")) {
98+
this.status = currentText.toString().trim();
99+
}
100+
currentText = new StringBuilder();
101+
}
102+
103+
@Override
104+
public void characters(char ch[], int start, int length) {
105+
currentText.append(ch, start, length);
106+
}
107+
}
108+
109+
static class NetworkStatusHandler extends DefaultHandler implements
110+
SAXDecoder.ContentHandlerWithResult<NetworkStatus> {
111+
@Inject NetworkStatusHandler() {
112+
}
113+
114+
private StringBuilder currentText = new StringBuilder();
115+
116+
private NetworkStatus status;
117+
118+
@Override
119+
public NetworkStatus result() {
120+
return status;
121+
}
122+
123+
@Override
124+
public void endElement(String uri, String name, String qName) {
125+
if (qName.equals("NeustarNetworkStatus")) {
126+
this.status = NetworkStatus.valueOf(currentText.toString().trim().toUpperCase());
127+
}
128+
currentText = new StringBuilder();
129+
}
130+
131+
@Override
132+
public void characters(char ch[], int start, int length) {
133+
currentText.append(ch, start, length);
134+
}
135+
}
136+
}

core/src/test/java/feign/examples/IAMExample.java

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,28 @@
2323
import feign.RequestTemplate;
2424
import feign.Target;
2525
import feign.codec.Decoder;
26-
import feign.codec.Decoders;
26+
import feign.codec.Decoders.ApplyFirstGroup;
27+
import feign.codec.Decoders.TransformFirstGroup;
28+
import feign.codec.SAXDecoder;
29+
import org.xml.sax.helpers.DefaultHandler;
30+
31+
import javax.inject.Inject;
32+
import javax.inject.Provider;
2733

2834
import static dagger.Provides.Type.SET;
2935

3036
public class IAMExample {
3137

3238
interface IAM {
33-
@RequestLine("GET /?Action=GetUser&Version=2010-05-08") String arn();
39+
@RequestLine("GET /?Action=GetUser&Version=2010-05-08") Long userId();
3440
}
3541

3642
public static void main(String... args) {
3743

38-
IAM iam = Feign.create(new IAMTarget(args[0], args[1]), new IAMModule());
39-
System.out.println(iam.arn());
44+
for (Object decodingApproach : new Object[]{new DecodeWithSax(), new DecodeWithRegEx()}) {
45+
IAM iam = Feign.create(new IAMTarget(args[0], args[1]), decodingApproach);
46+
System.out.println(iam.userId());
47+
}
4048
}
4149

4250
static class IAMTarget extends AWSSignatureVersion4 implements Target<IAM> {
@@ -64,9 +72,52 @@ private IAMTarget(String accessKey, String secretKey) {
6472
}
6573

6674
@Module(library = true)
67-
static class IAMModule {
68-
@Provides(type = SET) Decoder decoder() {
69-
return Decoders.firstGroup("<Arn>([\\S&&[^<]]+)</Arn>");
75+
static class DecodeWithRegEx {
76+
@Provides(type = SET) Decoder regExDecoder() {
77+
return new TransformFirstGroup<Long>("<UserId>([0-9]+)</UserId>", new ApplyFirstGroup<Long>() {
78+
79+
@Override public Long apply(String firstGroup) {
80+
return Long.parseLong(firstGroup);
81+
}
82+
}) {
83+
};
84+
}
85+
}
86+
87+
@Module(library = true)
88+
static class DecodeWithSax {
89+
@Provides(type = SET) Decoder saxDecoder(Provider<UserIdHandler> userIdHandler) {
90+
return SAXDecoder.builder() //
91+
.addContentHandler(userIdHandler) //
92+
.build();
93+
}
94+
}
95+
96+
static class UserIdHandler extends DefaultHandler implements
97+
SAXDecoder.ContentHandlerWithResult<Long> {
98+
@Inject UserIdHandler() {
99+
}
100+
101+
private StringBuilder currentText = new StringBuilder();
102+
103+
private Long userId;
104+
105+
@Override
106+
public Long result() {
107+
return userId;
108+
}
109+
110+
@Override
111+
public void endElement(String uri, String name, String qName) {
112+
if (qName.equals("UserId")) {
113+
this.userId = Long.parseLong(currentText.toString().trim());
114+
}
115+
currentText = new StringBuilder();
116+
}
117+
118+
@Override
119+
public void characters(char ch[], int start, int length) {
120+
currentText.append(ch, start, length);
70121
}
71122
}
72123
}

0 commit comments

Comments
 (0)