View Javadoc
1   /*
2    * Licensed under the Apache License, Version 2.0 (the "License");
3    * you may not use this file except in compliance with the License.
4    * You may obtain a copy of the License at
5    *
6    *     http://www.apache.org/licenses/LICENSE-2.0
7    *
8    * Unless required by applicable law or agreed to in writing, software
9    * distributed under the License is distributed on an "AS IS" BASIS,
10   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11   * See the License for the specific language governing permissions and
12   * limitations under the License.
13   */
14  package org.gbif.ws.client;
15  
16  import org.gbif.ws.security.Md5EncodeService;
17  import org.gbif.ws.security.Md5EncodeServiceImpl;
18  import org.gbif.ws.security.SecretKeySigningService;
19  import org.gbif.ws.security.SigningService;
20  
21  import java.nio.charset.Charset;
22  import java.nio.charset.StandardCharsets;
23  import java.time.Duration;
24  import java.util.concurrent.TimeUnit;
25  
26  import org.apache.http.client.HttpClient;
27  import org.apache.http.client.config.RequestConfig;
28  import org.apache.http.config.ConnectionConfig;
29  import org.apache.http.config.SocketConfig;
30  import org.apache.http.impl.client.HttpClients;
31  
32  import com.fasterxml.jackson.databind.ObjectMapper;
33  
34  import feign.Contract;
35  import feign.Feign;
36  import feign.InvocationHandlerFactory;
37  import feign.Request;
38  import feign.RequestInterceptor;
39  import feign.Retryer;
40  import feign.codec.Decoder;
41  import feign.codec.Encoder;
42  import feign.codec.ErrorDecoder;
43  import feign.form.spring.SpringFormEncoder;
44  import feign.httpclient.ApacheHttpClient;
45  import lombok.Builder;
46  import lombok.Data;
47  
48  /**
49   * ClientBuilder used to create Feign Clients.
50   * This builders support retry using exponential backoff and multithreaded http client.
51   */
52  @SuppressWarnings("unused")
53  public class ClientBuilder {
54  
55    private static final String HTTP_PROTOCOL = "http";
56    private static final String HTTPS_PROTOCOL = "https";
57  
58    private String url;
59    private long connectTimeoutMillis = 10_000;
60    private long readTimeoutMillis = 60_000;
61    private RequestInterceptor requestInterceptor;
62    private Decoder decoder;
63    private Encoder encoder;
64    private ConnectionPoolConfig connectionPoolConfig;
65    private ObjectMapper objectMapper;
66    private Retryer retryer;
67    private boolean formEncoder;
68  
69    private Contract contract;
70    private ErrorDecoder errorDecoder;
71    private InvocationHandlerFactory invocationHandlerFactory;
72  
73    /**
74     * Exponential backoff retryer.
75     */
76    public ClientBuilder withExponentialBackoffRetry(
77        Duration initialInterval, double multiplier, int maxAttempts) {
78      retryer = new ClientRetryer(initialInterval.toMillis(), maxAttempts, multiplier);
79      return this;
80    }
81  
82    /**
83     * Target base url.
84     */
85    public ClientBuilder withUrl(String url) {
86      this.url = url;
87      return this;
88    }
89  
90    /**
91     * Simple base credentials.
92     */
93    public ClientBuilder withCredentials(String username, String password) {
94      this.requestInterceptor = new SimpleUserAuthRequestInterceptor(username, password);
95      return this;
96    }
97  
98    /**
99     * Custom AppKey credentials.
100    */
101   public ClientBuilder withAppKeyCredentials(String username, String appKey, String secretKey) {
102     this.requestInterceptor =
103         new GbifAuthRequestInterceptor(
104             username,
105             appKey,
106             secretKey,
107             new SecretKeySigningService(),
108             new Md5EncodeServiceImpl(objectMapper));
109     return this;
110   }
111 
112   /**
113    * Connection pool configuration to create a multithreaded client.
114    */
115   public ClientBuilder withConnectionPoolConfig(ConnectionPoolConfig connectionPoolConfig) {
116     this.connectionPoolConfig = connectionPoolConfig;
117     return this;
118   }
119 
120   /**
121    * Client connection timeout in milliseconds.
122    */
123   public ClientBuilder withConnectTimeout(int connectTimeoutMillis) {
124     this.connectTimeoutMillis = connectTimeoutMillis;
125     return this;
126   }
127 
128   /**
129    * Client read timeout in milliseconds.
130    */
131   public ClientBuilder withReadTimeout(int readTimeoutMillis) {
132     this.readTimeoutMillis = readTimeoutMillis;
133     return this;
134   }
135 
136   /**
137    * Jakcson ObjectMapper used to serialize JSON data.
138    */
139   public ClientBuilder withObjectMapper(ObjectMapper objectMapper) {
140     this.objectMapper = objectMapper;
141     this.encoder = new ClientEncoder(objectMapper);
142     this.decoder = new ClientDecoder(objectMapper);
143     return this;
144   }
145 
146   /**
147    * Custom GBIF authentication.
148    */
149   public ClientBuilder withCustomGbifAuth(
150       String username,
151       String appKey,
152       String secretKey,
153       SigningService signingService,
154       Md5EncodeService md5EncodeService) {
155     this.requestInterceptor =
156         new GbifAuthRequestInterceptor(
157             username, appKey, secretKey, signingService, md5EncodeService);
158     return this;
159   }
160 
161   public ClientBuilder withFormEncoder() {
162     this.formEncoder = true;
163     return this;
164   }
165 
166   public ClientBuilder withClientContract(ClientContract clientContract) {
167     this.contract = clientContract;
168     return this;
169   }
170 
171   /**
172    * Creates a new client instance.
173    */
174   public <T> T build(Class<T> clazz) {
175     Feign.Builder builder =
176         Feign.builder()
177             .encoder(formEncoder ? new SpringFormEncoder(encoder) : encoder)
178             .decoder(decoder)
179             .errorDecoder(errorDecoder != null ? errorDecoder : new ClientErrorDecoder())
180             .contract(contract != null ? contract : ClientContract.withDefaultProcessors())
181             .options(
182                 new Request.Options(
183                     connectTimeoutMillis,
184                     TimeUnit.MILLISECONDS,
185                     readTimeoutMillis,
186                     TimeUnit.MILLISECONDS,
187                     true))
188             .decode404()
189             .invocationHandlerFactory(
190                 invocationHandlerFactory != null
191                     ? invocationHandlerFactory
192                     : new ClientInvocationHandlerFactory());
193 
194     if (retryer != null) {
195       builder.retryer(retryer);
196     }
197 
198     if (requestInterceptor != null) {
199       builder.requestInterceptor(requestInterceptor);
200     }
201 
202     if (connectionPoolConfig != null) {
203       builder.client(new ApacheHttpClient(newMultithreadedClient(connectionPoolConfig)));
204     }
205 
206     return builder.target(clazz, url);
207   }
208 
209   /**
210    * Creates a Http multithreaded client.
211    */
212   private static HttpClient newMultithreadedClient(ConnectionPoolConfig connectionPoolConfig) {
213 
214     return HttpClients.custom()
215         .setMaxConnTotal(connectionPoolConfig.getMaxConnections())
216         .setMaxConnPerRoute(connectionPoolConfig.getMaxPerRoute())
217         .setDefaultSocketConfig(
218             SocketConfig.custom().setSoTimeout(connectionPoolConfig.getTimeout()).build())
219         .setDefaultConnectionConfig(
220             ConnectionConfig.custom()
221                 .setCharset(Charset.forName(StandardCharsets.UTF_8.name()))
222                 .build())
223         .setDefaultRequestConfig(
224             RequestConfig.custom()
225                 .setConnectTimeout(connectionPoolConfig.getTimeout())
226                 .setConnectionRequestTimeout(connectionPoolConfig.getTimeout())
227                 .build())
228         .build();
229   }
230 
231   @Data
232   @Builder
233   public static class ConnectionPoolConfig {
234     private final Integer timeout;
235     private final Integer maxConnections;
236     private final Integer maxPerRoute;
237   }
238 }