/*
* 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;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import feign.Logger.NoOpLogger;
import feign.ReflectiveFeign.ParseHandlersByName;
import feign.Request.Options;
import feign.Target.HardCodedTarget;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.codec.ErrorDecoder;
/**
* Feign's purpose is to ease development against http apis that feign restfulness. <br> In
* implementation, Feign is a {@link Feign#newInstance factory} for generating {@link Target
* targeted} http apis.
*/
public abstract class Feign {
public static Builder builder() {
return new Builder();
}
/**
* Configuration keys are formatted as unresolved <a href= "http://docs.oracle.com/javase/6/docs/jdk/api/javadoc/doclet/com/sun/javadoc/SeeTag.html"
* >see tags</a>. This method exposes that format, in case you need to create the same value as
* {@link MethodMetadata#configKey()} for correlation purposes.
*
* <p>Here are some sample encodings:
*
* <pre>
* <ul>
* <li>{@code Route53}: would match a class {@code route53.Route53}</li>
* <li>{@code Route53#list()}: would match a method {@code route53.Route53#list()}</li>
* <li>{@code Route53#listAt(Marker)}: would match a method {@code
* route53.Route53#listAt(Marker)}</li>
* <li>{@code Route53#listByNameAndType(String, String)}: would match a method {@code
* route53.Route53#listAt(String, String)}</li>
* </ul>
* </pre>
*
* Note that there is no whitespace expected in a key!
*
* @param targetType {@link feign.Target#type() type} of the Feign interface.
* @param method invoked method, present on {@code type} or its super.
* @see MethodMetadata#configKey()
*/
public static String configKey(Class targetType, Method method) {
StringBuilder builder = new StringBuilder();
builder.append(targetType.getSimpleName());
builder.append('#').append(method.getName()).append('(');
for (Type param : method.getGenericParameterTypes()) {
param = Types.resolve(targetType, targetType, param);
builder.append(Types.getRawType(param).getSimpleName()).append(',');
}
if (method.getParameterTypes().length > 0) {
builder.deleteCharAt(builder.length() - 1);
}
return builder.append(')').toString();
}
/**
* @deprecated use {@link #configKey(Class, Method)} instead.
*/
@Deprecated
public static String configKey(Method method) {
return configKey(method.getDeclaringClass(), method);
}
/**
* Returns a new instance of an HTTP API, defined by annotations in the {@link Feign Contract},
* for the specified {@code target}. You should cache this result.
*/
public abstract <T> T newInstance(Target<T> target);
public static class Builder {
private final List<RequestInterceptor> requestInterceptors =
new ArrayList<RequestInterceptor>();
private Logger.Level logLevel = Logger.Level.NONE;
private Contract contract = new Contract.Default();
private Client client = new Client.Default(null, null);
private Retryer retryer = new Retryer.Default();
private Logger logger = new NoOpLogger();
private Encoder encoder = new Encoder.Default();
private Decoder decoder = new Decoder.Default();
private ErrorDecoder errorDecoder = new ErrorDecoder.Default();
private Options options = new Options();
private InvocationHandlerFactory invocationHandlerFactory =
new InvocationHandlerFactory.Default();
private boolean decode404;
public Builder logLevel(Logger.Level logLevel) {
this.logLevel = logLevel;
return this;
}
public Builder contract(Contract contract) {
this.contract = contract;
return this;
}
public Builder client(Client client) {
this.client = client;
return this;
}
public Builder retryer(Retryer retryer) {
this.retryer = retryer;
return this;
}
public Builder logger(Logger logger) {
this.logger = logger;
return this;
}
public Builder encoder(Encoder encoder) {
this.encoder = encoder;
return this;
}
public Builder decoder(Decoder decoder) {
this.decoder = decoder;
return this;
}
/**
* Allows to map the response before passing it to the decoder.
*/
public Builder mapAndDecode(ResponseMapper mapper, Decoder decoder) {
this.decoder = new ResponseMappingDecoder(mapper, decoder);
return this;
}
/**
* This flag indicates that the {@link #decoder(Decoder) decoder} should process responses with
* 404 status, specifically returning null or empty instead of throwing {@link FeignException}.
*
* <p/> All first-party (ex gson) decoders return well-known empty values defined by {@link
* Util#emptyValueOf}. To customize further, wrap an existing {@link #decoder(Decoder) decoder}
* or make your own.
*
* <p/> This flag only works with 404, as opposed to all or arbitrary status codes. This was an
* explicit decision: 404 -> empty is safe, common and doesn't complicate redirection, retry or
* fallback policy. If your server returns a different status for not-found, correct via a
* custom {@link #client(Client) client}.
*
* @since 8.12
*/
public Builder decode404() {
this.decode404 = true;
return this;
}
public Builder errorDecoder(ErrorDecoder errorDecoder) {
this.errorDecoder = errorDecoder;
return this;
}
public Builder options(Options options) {
this.options = options;
return this;
}
/**
* Adds a single request interceptor to the builder.
*/
public Builder requestInterceptor(RequestInterceptor requestInterceptor) {
this.requestInterceptors.add(requestInterceptor);
return this;
}
/**
* Sets the full set of request interceptors for the builder, overwriting any previous
* interceptors.
*/
public Builder requestInterceptors(Iterable<RequestInterceptor> requestInterceptors) {
this.requestInterceptors.clear();
for (RequestInterceptor requestInterceptor : requestInterceptors) {
this.requestInterceptors.add(requestInterceptor);
}
return this;
}
/**
* Allows you to override how reflective dispatch works inside of Feign.
*/
public Builder invocationHandlerFactory(InvocationHandlerFactory invocationHandlerFactory) {
this.invocationHandlerFactory = invocationHandlerFactory;
return this;
}
public <T> T target(Class<T> apiType, String url) {
return target(new HardCodedTarget<T>(apiType, url));
}
public <T> T target(Target<T> target) {
return build().newInstance(target);
}
public Feign build() {
SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
logLevel, decode404);
ParseHandlersByName handlersByName =
new ParseHandlersByName(contract, options, encoder, decoder,
errorDecoder, synchronousMethodHandlerFactory);
return new ReflectiveFeign(handlersByName, invocationHandlerFactory);
}
}
static class ResponseMappingDecoder implements Decoder {
private final ResponseMapper mapper;
private final Decoder delegate;
ResponseMappingDecoder(ResponseMapper mapper, Decoder decoder) {
this.mapper = mapper;
this.delegate = decoder;
}
@Override
public Object decode(Response response, Type type) throws IOException {
return delegate.decode(mapper.map(response, type), type);
}
}
}