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}