/* * 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.sax; import feign.Response; import feign.codec.DecodeException; import feign.codec.Decoder; import org.xml.sax.ContentHandler; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import org.xml.sax.helpers.XMLReaderFactory; import javax.inject.Provider; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Constructor; 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.ensureClosed; import static feign.Util.resolveLastTypeParameter; /** * Decodes responses using SAX, which is supported both in normal JVM environments, as well Android. *
*

Basic example with with Feign.Builder

*
*
 * api = Feign.builder()
 *            .decoder(SAXDecoder.builder()
 *                               .registerContentHandler(ContentHandlerForFoo.class)
 *                               .registerContentHandler(ContentHandlerForBar.class)
 *                               .build())
 *            .target(MyApi.class, "http://api");
 * 
*

*

Advanced example with Dagger

*
*
 * @Provides
 * Decoder saxDecoder(Provider<ContentHandlerForFoo> foo, //
 *         Provider<ContentHandlerForBar> bar) {
 *     return SAXDecoder.builder() //
 *             .registerContentHandler(Foo.class, foo) //
 *             .registerContentHandler(Bar.class, bar) //
 *             .build();
 * }
 * 
*/ public class SAXDecoder implements Decoder { 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>>(); /** * Will call {@link Constructor#newInstance(Object...)} on {@code handlerClass} for each content stream. *

*

Note

*
* While this is costly vs {@code new}, it may not affect real performance due to the high cost of reading streams. * * @throws IllegalArgumentException if there's no no-arg constructor on {@code handlerClass}. */ public > Builder registerContentHandler(Class handlerClass) { Type type = resolveLastTypeParameter(checkNotNull(handlerClass, "handlerClass"), ContentHandlerWithResult.class); return registerContentHandler(type, new NewInstanceProvider(handlerClass)); } private static class NewInstanceProvider> implements Provider { private final Constructor ctor; private NewInstanceProvider(Class clazz) { try { this.ctor = clazz.getDeclaredConstructor(); // allow private or package protected ctors ctor.setAccessible(true); } catch (NoSuchMethodException e) { throw new IllegalArgumentException("ensure " + clazz + " has a no-args constructor", e); } } @Override public T get() { try { return ctor.newInstance(); } catch (Exception e) { throw new IllegalArgumentException("exception attempting to instantiate " + ctor, e); } } } /** * Will call {@link Provider#get()} on {@code handler} for each content stream. * The {@code handler} is expected to have a generic parameter of {@code type}. */ public Builder registerContentHandler(Type type, Provider> handler) { this.handlerProviders.put(checkNotNull(type, "type"), checkNotNull(handler, "handler")); return this; } public SAXDecoder build() { return new SAXDecoder(handlerProviders); } } /** * Implementations are not intended to be shared across requests. */ public interface ContentHandlerWithResult extends ContentHandler { /** * expected to be set following a call to {@link XMLReader#parse(InputSource)} */ T result(); } private final Map>> handlerProviders; private SAXDecoder(Map>> handlerProviders) { this.handlerProviders = handlerProviders; } @Override public Object decode(Response response, Type type) throws IOException, DecodeException { if (response.body() == null) { return null; } 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); xmlReader.setFeature("http://xml.org/sax/features/validation", false); xmlReader.setContentHandler(handler); InputStream inputStream = response.body().asInputStream(); try { xmlReader.parse(new InputSource(inputStream)); } finally { ensureClosed(inputStream); } return handler.result(); } catch (SAXException e) { throw new DecodeException(e.getMessage(), e); } } }