001/*
002 * Licensed under the Apache License, Version 2.0 (the "License");
003 * you may not use this file except in compliance with the License.
004 * You may obtain a copy of the License at
005 *
006 *     http://www.apache.org/licenses/LICENSE-2.0
007 *
008 * Unless required by applicable law or agreed to in writing, software
009 * distributed under the License is distributed on an "AS IS" BASIS,
010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
011 * See the License for the specific language governing permissions and
012 * limitations under the License.
013 */
014package org.gbif.ws.client;
015
016import org.gbif.ws.security.Md5EncodeService;
017import org.gbif.ws.security.Md5EncodeServiceImpl;
018import org.gbif.ws.security.SecretKeySigningService;
019import org.gbif.ws.security.SigningService;
020
021import java.nio.charset.Charset;
022import java.nio.charset.StandardCharsets;
023import java.time.Duration;
024import java.util.concurrent.TimeUnit;
025
026import org.apache.http.client.HttpClient;
027import org.apache.http.client.config.RequestConfig;
028import org.apache.http.config.ConnectionConfig;
029import org.apache.http.config.SocketConfig;
030import org.apache.http.impl.client.HttpClients;
031
032import com.fasterxml.jackson.databind.ObjectMapper;
033
034import feign.Contract;
035import feign.Feign;
036import feign.InvocationHandlerFactory;
037import feign.Request;
038import feign.RequestInterceptor;
039import feign.Retryer;
040import feign.codec.Decoder;
041import feign.codec.Encoder;
042import feign.codec.ErrorDecoder;
043import feign.form.spring.SpringFormEncoder;
044import feign.httpclient.ApacheHttpClient;
045import lombok.Builder;
046import lombok.Data;
047
048/**
049 * ClientBuilder used to create Feign Clients.
050 * This builders support retry using exponential backoff and multithreaded http client.
051 */
052@SuppressWarnings("unused")
053public class ClientBuilder {
054
055  private static final String HTTP_PROTOCOL = "http";
056  private static final String HTTPS_PROTOCOL = "https";
057
058  private String url;
059  private long connectTimeoutMillis = 10_000;
060  private long readTimeoutMillis = 60_000;
061  private RequestInterceptor requestInterceptor;
062  private Decoder decoder;
063  private Encoder encoder;
064  private ConnectionPoolConfig connectionPoolConfig;
065  private ObjectMapper objectMapper;
066  private Retryer retryer;
067  private boolean formEncoder;
068
069  private Contract contract;
070  private ErrorDecoder errorDecoder;
071  private InvocationHandlerFactory invocationHandlerFactory;
072
073  /**
074   * Exponential backoff retryer.
075   */
076  public ClientBuilder withExponentialBackoffRetry(
077      Duration initialInterval, double multiplier, int maxAttempts) {
078    retryer = new ClientRetryer(initialInterval.toMillis(), maxAttempts, multiplier);
079    return this;
080  }
081
082  /**
083   * Target base url.
084   */
085  public ClientBuilder withUrl(String url) {
086    this.url = url;
087    return this;
088  }
089
090  /**
091   * Simple base credentials.
092   */
093  public ClientBuilder withCredentials(String username, String password) {
094    this.requestInterceptor = new SimpleUserAuthRequestInterceptor(username, password);
095    return this;
096  }
097
098  /**
099   * 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}