From 0ac4f9abea7b0a60d966e8451515ce0e4ebf1558 Mon Sep 17 00:00:00 2001 From: adriancole Date: Wed, 11 Sep 2013 22:03:46 +0200 Subject: [PATCH] SaxDecoder now decodes multiple types. --- CHANGES.md | 1 + .../src/main/java/feign/codec/SAXDecoder.java | 67 ++++++--- .../test/java/feign/codec/SAXDecoderTest.java | 136 ++++++++++++++++++ .../test/java/feign/examples/IAMExample.java | 65 ++++++++- 4 files changed, 244 insertions(+), 25 deletions(-) create mode 100644 core/src/test/java/feign/codec/SAXDecoderTest.java diff --git a/CHANGES.md b/CHANGES.md index 3cf46c2c..7743e878 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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. diff --git a/core/src/main/java/feign/codec/SAXDecoder.java b/core/src/main/java/feign/codec/SAXDecoder.java index 972fee9c..48481adb 100644 --- a/core/src/main/java/feign/codec/SAXDecoder.java +++ b/core/src/main/java/feign/codec/SAXDecoder.java @@ -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}. + *

+ * + *

+ * @Provides(type = SET)
+ * Decoder saxDecoder(Provider<ContentHandlerForFoo> foo, //
+ *         Provider<ContentHandlerForBar> bar) {
+ *     return SAXDecoder.builder() //
+ *             .addContentHandler(foo) //
+ *             .addContentHandler(bar) //
+ *             .build();
+ * }
+ * 
+ */ +public class SAXDecoder implements Decoder.TextStream { + + 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>> handlerProviders = + new LinkedHashMap>>(); + + public Builder addContentHandler(Provider> 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 implements Decoder.TextStream { /* Implementations are not intended to be shared across requests. */ public interface ContentHandlerWithResult extends ContentHandler { /* @@ -39,27 +80,17 @@ public class SAXDecoder implements Decoder.TextStream { T result(); } - private final Provider> handlers; + private final Map>> 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. - *

- *
- *

- *

-   * new SaxDecoder<Foo>(fooHandlers) {
-   * }; // note the curly braces ensures no type erasure!
-   * 
- */ - protected SAXDecoder(Provider> handlers) { - this.handlers = checkNotNull(handlers, "handlers"); + private SAXDecoder(Map>> handlerProviders) { + this.handlerProviders = handlerProviders; } @Override - public T decode(Reader reader, Type type) throws IOException, DecodeException { - ContentHandlerWithResult 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> 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); diff --git a/core/src/test/java/feign/codec/SAXDecoderTest.java b/core/src/test/java/feign/codec/SAXDecoderTest.java new file mode 100644 index 00000000..01f0d75d --- /dev/null +++ b/core/src/test/java/feign/codec/SAXDecoderTest.java @@ -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 networkStatus, // + Provider networkStatusAsString) { + return SAXDecoder.builder() // + .addContentHandler(networkStatus) // + .addContentHandler(networkStatusAsString) // + .build(); + } + } + + @Inject Set 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 = ""// + + "\n"// + + " \n"// + + " \n"// + + " Failed\n"// + + " \n"// + + " \n"// + + ""; + + static enum NetworkStatus { + GOOD, FAILED; + } + + static class NetworkStatusStringHandler extends DefaultHandler implements + SAXDecoder.ContentHandlerWithResult { + @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 { + @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); + } + } +} diff --git a/core/src/test/java/feign/examples/IAMExample.java b/core/src/test/java/feign/examples/IAMExample.java index f16bb3c2..0a7c63fa 100644 --- a/core/src/test/java/feign/examples/IAMExample.java +++ b/core/src/test/java/feign/examples/IAMExample.java @@ -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 { @@ -64,9 +72,52 @@ public class IAMExample { } @Module(library = true) - static class IAMModule { - @Provides(type = SET) Decoder decoder() { - return Decoders.firstGroup("([\\S&&[^<]]+)"); + static class DecodeWithRegEx { + @Provides(type = SET) Decoder regExDecoder() { + return new TransformFirstGroup("([0-9]+)", new ApplyFirstGroup() { + + @Override public Long apply(String firstGroup) { + return Long.parseLong(firstGroup); + } + }) { + }; + } + } + + @Module(library = true) + static class DecodeWithSax { + @Provides(type = SET) Decoder saxDecoder(Provider userIdHandler) { + return SAXDecoder.builder() // + .addContentHandler(userIdHandler) // + .build(); + } + } + + static class UserIdHandler extends DefaultHandler implements + SAXDecoder.ContentHandlerWithResult { + @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); } } }