Browse Source

SaxDecoder now decodes multiple types.

pull/63/head
adriancole 11 years ago
parent
commit
0ac4f9abea
  1. 1
      CHANGES.md
  2. 67
      core/src/main/java/feign/codec/SAXDecoder.java
  3. 136
      core/src/test/java/feign/codec/SAXDecoderTest.java
  4. 65
      core/src/test/java/feign/examples/IAMExample.java

1
CHANGES.md

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
### Version 5.0
* Remove support for Observable methods.
* SaxDecoder now decodes multiple types.
### Version 4.4.1
* Fix NullPointerException on calling equals and hashCode.

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

@ -25,11 +25,52 @@ import javax.inject.Provider; @@ -25,11 +25,52 @@ import javax.inject.Provider;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.Type;
import java.util.LinkedHashMap;
import java.util.Map;
import static feign.Util.checkNotNull;
import static feign.Util.checkState;
import static feign.Util.resolveLastTypeParameter;
/**
* Decodes responses using SAX. Configure using the {@link SAXDecoder.Builder
* builder}.
* <p/>
*
* <pre>
* &#064;Provides(type = SET)
* Decoder saxDecoder(Provider&lt;ContentHandlerForFoo&gt; foo, //
* Provider&lt;ContentHandlerForBar&gt; bar) {
* return SAXDecoder.builder() //
* .addContentHandler(foo) //
* .addContentHandler(bar) //
* .build();
* }
* </pre>
*/
public class SAXDecoder implements Decoder.TextStream<Object> {
public static Builder builder() {
return new Builder();
}
// builder as dagger doesn't support wildcard bindings, map bindings, or set bindings of providers.
public static class Builder {
private final Map<Type, Provider<? extends ContentHandlerWithResult<?>>> handlerProviders =
new LinkedHashMap<Type, Provider<? extends ContentHandlerWithResult<?>>>();
public Builder addContentHandler(Provider<? extends ContentHandlerWithResult<?>> handler) {
Type type = resolveLastTypeParameter(checkNotNull(handler, "handler").getClass(), Provider.class);
type = resolveLastTypeParameter(type, ContentHandlerWithResult.class);
this.handlerProviders.put(type, handler);
return this;
}
public SAXDecoder build() {
return new SAXDecoder(handlerProviders);
}
}
public class SAXDecoder<T> implements Decoder.TextStream<T> {
/* Implementations are not intended to be shared across requests. */
public interface ContentHandlerWithResult<T> extends ContentHandler {
/*
@ -39,27 +80,17 @@ public class SAXDecoder<T> implements Decoder.TextStream<T> { @@ -39,27 +80,17 @@ public class SAXDecoder<T> implements Decoder.TextStream<T> {
T result();
}
private final Provider<? extends ContentHandlerWithResult<T>> handlers;
private final Map<Type, Provider<? extends ContentHandlerWithResult<?>>> handlerProviders;
/**
* You must subclass this, in order to prevent type erasure on {@code T}. In
* addition to making a concrete type, you can also use the following form.
* <p/>
* <br>
* <p/>
* <pre>
* new SaxDecoder&lt;Foo&gt;(fooHandlers) {
* }; // note the curly braces ensures no type erasure!
* </pre>
*/
protected SAXDecoder(Provider<? extends ContentHandlerWithResult<T>> handlers) {
this.handlers = checkNotNull(handlers, "handlers");
private SAXDecoder(Map<Type, Provider<? extends ContentHandlerWithResult<?>>> handlerProviders) {
this.handlerProviders = handlerProviders;
}
@Override
public T decode(Reader reader, Type type) throws IOException, DecodeException {
ContentHandlerWithResult<T> handler = handlers.get();
checkState(handler != null, "%s returned null for type %s", this, type);
public Object decode(Reader reader, Type type) throws IOException, DecodeException {
Provider<? extends ContentHandlerWithResult<?>> handlerProvider = handlerProviders.get(type);
checkState(handlerProvider != null, "type %s not in configured handlers %s", type, handlerProviders.keySet());
ContentHandlerWithResult<?> handler = handlerProvider.get();
try {
XMLReader xmlReader = XMLReaderFactory.createXMLReader();
xmlReader.setFeature("http://xml.org/sax/features/namespaces", false);

136
core/src/test/java/feign/codec/SAXDecoderTest.java

@ -0,0 +1,136 @@ @@ -0,0 +1,136 @@
/*
* Copyright 2013 Netflix, Inc.
*
* 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 dagger.ObjectGraph;
import dagger.Provides;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import org.xml.sax.helpers.DefaultHandler;
import javax.inject.Inject;
import javax.inject.Provider;
import java.io.IOException;
import java.io.StringReader;
import java.text.ParseException;
import java.util.Set;
import static dagger.Provides.Type.SET;
import static org.testng.Assert.assertEquals;
// unbound wildcards are not currently injectable in dagger.
@SuppressWarnings("rawtypes")
public class SAXDecoderTest {
@dagger.Module(injects = SAXDecoderTest.class)
static class Module {
@Provides(type = SET) Decoder saxDecoder(Provider<NetworkStatusHandler> networkStatus, //
Provider<NetworkStatusStringHandler> networkStatusAsString) {
return SAXDecoder.builder() //
.addContentHandler(networkStatus) //
.addContentHandler(networkStatusAsString) //
.build();
}
}
@Inject Set<Decoder> decoders;
@BeforeClass void inject() {
ObjectGraph.create(new Module()).inject(this);
}
@Test public void parsesConfiguredTypes() throws ParseException, IOException {
Decoder decoder = decoders.iterator().next();
assertEquals(decoder.decode(new StringReader(statusFailed), NetworkStatus.class), NetworkStatus.FAILED);
assertEquals(decoder.decode(new StringReader(statusFailed), String.class), "Failed");
}
@Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp =
"type int not in configured handlers \\[class .*NetworkStatus, class java.lang.String\\]")
public void niceErrorOnUnconfiguredType() throws ParseException, IOException {
Decoder decoder = decoders.iterator().next();
decoder.decode(new StringReader(statusFailed), int.class);
}
static String statusFailed = ""//
+ "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">\n"//
+ " <soap:Body>\n"//
+ " <ns1:getNeustarNetworkStatusResponse xmlns:ns1=\"http://webservice.api.ultra.neustar.com/v01/\">\n"//
+ " <NeustarNetworkStatus xmlns:ns2=\"http://schema.ultraservice.neustar.com/v01/\">Failed</NeustarNetworkStatus>\n"//
+ " </ns1:getNeustarNetworkStatusResponse>\n"//
+ " </soap:Body>\n"//
+ "</soap:Envelope>";
static enum NetworkStatus {
GOOD, FAILED;
}
static class NetworkStatusStringHandler extends DefaultHandler implements
SAXDecoder.ContentHandlerWithResult<String> {
@Inject NetworkStatusStringHandler() {
}
private StringBuilder currentText = new StringBuilder();
private String status;
@Override
public String result() {
return status;
}
@Override
public void endElement(String uri, String name, String qName) {
if (qName.equals("NeustarNetworkStatus")) {
this.status = currentText.toString().trim();
}
currentText = new StringBuilder();
}
@Override
public void characters(char ch[], int start, int length) {
currentText.append(ch, start, length);
}
}
static class NetworkStatusHandler extends DefaultHandler implements
SAXDecoder.ContentHandlerWithResult<NetworkStatus> {
@Inject NetworkStatusHandler() {
}
private StringBuilder currentText = new StringBuilder();
private NetworkStatus status;
@Override
public NetworkStatus result() {
return status;
}
@Override
public void endElement(String uri, String name, String qName) {
if (qName.equals("NeustarNetworkStatus")) {
this.status = NetworkStatus.valueOf(currentText.toString().trim().toUpperCase());
}
currentText = new StringBuilder();
}
@Override
public void characters(char ch[], int start, int length) {
currentText.append(ch, start, length);
}
}
}

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

@ -23,20 +23,28 @@ import feign.RequestLine; @@ -23,20 +23,28 @@ import feign.RequestLine;
import feign.RequestTemplate;
import feign.Target;
import feign.codec.Decoder;
import feign.codec.Decoders;
import feign.codec.Decoders.ApplyFirstGroup;
import feign.codec.Decoders.TransformFirstGroup;
import feign.codec.SAXDecoder;
import org.xml.sax.helpers.DefaultHandler;
import javax.inject.Inject;
import javax.inject.Provider;
import static dagger.Provides.Type.SET;
public class IAMExample {
interface IAM {
@RequestLine("GET /?Action=GetUser&Version=2010-05-08") String arn();
@RequestLine("GET /?Action=GetUser&Version=2010-05-08") Long userId();
}
public static void main(String... args) {
IAM iam = Feign.create(new IAMTarget(args[0], args[1]), new IAMModule());
System.out.println(iam.arn());
for (Object decodingApproach : new Object[]{new DecodeWithSax(), new DecodeWithRegEx()}) {
IAM iam = Feign.create(new IAMTarget(args[0], args[1]), decodingApproach);
System.out.println(iam.userId());
}
}
static class IAMTarget extends AWSSignatureVersion4 implements Target<IAM> {
@ -64,9 +72,52 @@ public class IAMExample { @@ -64,9 +72,52 @@ public class IAMExample {
}
@Module(library = true)
static class IAMModule {
@Provides(type = SET) Decoder decoder() {
return Decoders.firstGroup("<Arn>([\\S&&[^<]]+)</Arn>");
static class DecodeWithRegEx {
@Provides(type = SET) Decoder regExDecoder() {
return new TransformFirstGroup<Long>("<UserId>([0-9]+)</UserId>", new ApplyFirstGroup<Long>() {
@Override public Long apply(String firstGroup) {
return Long.parseLong(firstGroup);
}
}) {
};
}
}
@Module(library = true)
static class DecodeWithSax {
@Provides(type = SET) Decoder saxDecoder(Provider<UserIdHandler> userIdHandler) {
return SAXDecoder.builder() //
.addContentHandler(userIdHandler) //
.build();
}
}
static class UserIdHandler extends DefaultHandler implements
SAXDecoder.ContentHandlerWithResult<Long> {
@Inject UserIdHandler() {
}
private StringBuilder currentText = new StringBuilder();
private Long userId;
@Override
public Long result() {
return userId;
}
@Override
public void endElement(String uri, String name, String qName) {
if (qName.equals("UserId")) {
this.userId = Long.parseLong(currentText.toString().trim());
}
currentText = new StringBuilder();
}
@Override
public void characters(char ch[], int start, int length) {
currentText.append(ch, start, length);
}
}
}

Loading…
Cancel
Save