/* * Copyright © 2012 The Feign Authors (feign@commonhaus.dev) * * 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 static feign.ExceptionPropagationPolicy.NONE; import feign.Feign.ResponseMappingDecoder; import feign.Logger.NoOpLogger; import feign.Request.Options; import feign.codec.Codec; import feign.codec.Decoder; import feign.codec.DefaultDecoder; import feign.codec.DefaultEncoder; import feign.codec.DefaultErrorDecoder; import feign.codec.Encoder; import feign.codec.ErrorDecoder; import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; public abstract class BaseBuilder, T> implements Cloneable { private final B thisB; protected List requestInterceptors = new ArrayList<>(); protected List responseInterceptors = new ArrayList<>(); protected Logger.Level logLevel = Logger.Level.NONE; protected Contract contract = new DefaultContract(); protected Retryer retryer = new DefaultRetryer(); protected Logger logger = new NoOpLogger(); protected Encoder encoder = new DefaultEncoder(); protected Decoder decoder = new DefaultDecoder(); protected boolean closeAfterDecode = true; protected boolean decodeVoid = false; protected QueryMapEncoder queryMapEncoder = QueryMap.MapEncoder.FIELD.instance(); protected ErrorDecoder errorDecoder = new DefaultErrorDecoder(); protected Options options = new Options(); protected InvocationHandlerFactory invocationHandlerFactory = new DefaultInvocationHandlerFactory(); protected boolean dismiss404; protected ExceptionPropagationPolicy propagationPolicy = NONE; protected List capabilities = new ArrayList<>(); public BaseBuilder() { super(); thisB = (B) this; } public B logLevel(Logger.Level logLevel) { this.logLevel = logLevel; return thisB; } public B contract(Contract contract) { this.contract = contract; return thisB; } public B retryer(Retryer retryer) { this.retryer = retryer; return thisB; } public B logger(Logger logger) { this.logger = logger; return thisB; } public B encoder(Encoder encoder) { this.encoder = encoder; return thisB; } public B decoder(Decoder decoder) { this.decoder = decoder; return thisB; } public B codec(Codec codec) { this.encoder = codec.encoder(); this.decoder = codec.decoder(); return thisB; } /** * This flag indicates that the response should not be automatically closed upon completion of * decoding the message. This should be set if you plan on processing the response into a * lazy-evaluated construct, such as a {@link java.util.Iterator}. Feign standard decoders do not * have built in support for this flag. If you are using this flag, you MUST also use a custom * Decoder, and be sure to close all resources appropriately somewhere in the Decoder (you can use * {@link Util#ensureClosed} for convenience). * * @since 9.6 */ public B doNotCloseAfterDecode() { this.closeAfterDecode = false; return thisB; } public B decodeVoid() { this.decodeVoid = true; return thisB; } public B queryMapEncoder(QueryMapEncoder queryMapEncoder) { this.queryMapEncoder = queryMapEncoder; return thisB; } /** Allows to map the response before passing it to the decoder. */ public B mapAndDecode(ResponseMapper mapper, Decoder decoder) { this.decoder = new ResponseMappingDecoder(mapper, decoder); return thisB; } /** * 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}. * *

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. * *

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 11.9 */ public B dismiss404() { this.dismiss404 = true; return thisB; } /** * 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}. * *

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. * *

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 * @deprecated use {@link #dismiss404()} instead. */ @Deprecated public B decode404() { this.dismiss404 = true; return thisB; } public B errorDecoder(ErrorDecoder errorDecoder) { this.errorDecoder = errorDecoder; return thisB; } public B options(Options options) { this.options = options; return thisB; } /** Adds a single request interceptor to the builder. */ public B requestInterceptor(RequestInterceptor requestInterceptor) { this.requestInterceptors.add(requestInterceptor); return thisB; } /** * Sets the full set of request interceptors for the builder, overwriting any previous * interceptors. */ public B requestInterceptors(Iterable requestInterceptors) { this.requestInterceptors.clear(); for (RequestInterceptor requestInterceptor : requestInterceptors) { this.requestInterceptors.add(requestInterceptor); } return thisB; } /** * Sets the full set of request interceptors for the builder, overwriting any previous * interceptors. */ public B responseInterceptors(Iterable responseInterceptors) { this.responseInterceptors.clear(); for (ResponseInterceptor responseInterceptor : responseInterceptors) { this.responseInterceptors.add(responseInterceptor); } return thisB; } /** Adds a single response interceptor to the builder. */ public B responseInterceptor(ResponseInterceptor responseInterceptor) { this.responseInterceptors.add(responseInterceptor); return thisB; } /** Allows you to override how reflective dispatch works inside of Feign. */ public B invocationHandlerFactory(InvocationHandlerFactory invocationHandlerFactory) { this.invocationHandlerFactory = invocationHandlerFactory; return thisB; } public B exceptionPropagationPolicy(ExceptionPropagationPolicy propagationPolicy) { this.propagationPolicy = propagationPolicy; return thisB; } public B addCapability(Capability capability) { this.capabilities.add(capability); return thisB; } @SuppressWarnings("unchecked") B enrich() { if (capabilities.isEmpty()) { return thisB; } try { B clone = (B) thisB.clone(); getFieldsToEnrich() .forEach( field -> { field.setAccessible(true); try { final Object originalValue = field.get(clone); final Object enriched; if (originalValue instanceof List) { Type ownerType = ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0]; enriched = ((List) originalValue) .stream() .map( value -> Capability.enrich( value, (Class) ownerType, capabilities)) .collect(Collectors.toList()); } else { enriched = Capability.enrich(originalValue, field.getType(), capabilities); } field.set(clone, enriched); } catch (IllegalArgumentException | IllegalAccessException e) { throw new RuntimeException("Unable to enrich field " + field, e); } finally { field.setAccessible(false); } }); // enrich each request interceptor, then enrich the list as a whole RequestInterceptor[] requestArray = clone.requestInterceptors.toArray(new RequestInterceptor[0]); for (int i = 0; i < requestArray.length; i++) { requestArray[i] = (RequestInterceptor) Capability.enrich(requestArray[i], RequestInterceptor.class, capabilities); } RequestInterceptors requestInterceptors = (RequestInterceptors) Capability.enrich( new RequestInterceptors(Arrays.asList(requestArray)), RequestInterceptors.class, capabilities); clone.requestInterceptors = requestInterceptors.interceptors(); // enrich each response interceptor, then enrich the list as a whole ResponseInterceptor[] responseArray = clone.responseInterceptors.toArray(new ResponseInterceptor[0]); for (int i = 0; i < responseArray.length; i++) { responseArray[i] = (ResponseInterceptor) Capability.enrich(responseArray[i], ResponseInterceptor.class, capabilities); } ResponseInterceptors responseInterceptors = (ResponseInterceptors) Capability.enrich( new ResponseInterceptors(Arrays.asList(responseArray)), ResponseInterceptors.class, capabilities); clone.responseInterceptors = responseInterceptors.interceptors(); return clone; } catch (CloneNotSupportedException e) { throw new AssertionError(e); } } List getFieldsToEnrich() { return Util.allFields(getClass()).stream() // exclude anything generated by compiler .filter(field -> !field.isSynthetic()) // and capabilities itself .filter(field -> !Objects.equals(field.getName(), "capabilities")) // and thisB helper field .filter(field -> !Objects.equals(field.getName(), "thisB")) // interceptor lists are enriched per-element then as a whole via custom types .filter(field -> !Objects.equals(field.getName(), "requestInterceptors")) .filter(field -> !Objects.equals(field.getName(), "responseInterceptors")) // skip primitive types .filter(field -> !field.getType().isPrimitive()) // skip enumerations .filter(field -> !field.getType().isEnum()) .collect(Collectors.toList()); } public final T build() { return enrich().internalBuild(); } protected abstract T internalBuild(); protected ResponseInterceptor.Chain responseInterceptorChain() { ResponseInterceptor.Chain endOfChain = ResponseInterceptor.Chain.DEFAULT; ResponseInterceptor.Chain executionChain = this.responseInterceptors.stream() .reduce(ResponseInterceptor::andThen) .map(interceptor -> interceptor.apply(endOfChain)) .orElse(endOfChain); return (ResponseInterceptor.Chain) Capability.enrich(executionChain, ResponseInterceptor.Chain.class, capabilities); } }