1 package com.guinetik.rr.http;
2
3 import com.guinetik.rr.RocketRestConfig;
4 import com.guinetik.rr.RocketRestOptions;
5 import com.guinetik.rr.auth.RocketSSL;
6 import com.guinetik.rr.interceptor.InterceptingClient;
7 import com.guinetik.rr.interceptor.RequestInterceptor;
8 import com.guinetik.rr.interceptor.RetryInterceptor;
9
10 import java.util.ArrayList;
11 import java.util.List;
12 import java.util.concurrent.ExecutorService;
13 import java.util.function.Predicate;
14 import java.util.function.UnaryOperator;
15
16 /**
17 * Factory for creating and configuring HTTP clients with decorators and settings.
18 *
19 * <p>This factory provides a fluent builder API for constructing {@link RocketClient}
20 * instances with various configurations like circuit breakers, custom decorators,
21 * and async execution support.
22 *
23 * <h2>Simple Client</h2>
24 * <pre class="language-java"><code>
25 * RocketClient client = RocketClientFactory.builder("https://api.example.com")
26 * .build();
27 * </code></pre>
28 *
29 * <h2>With Circuit Breaker</h2>
30 * <pre class="language-java"><code>
31 * RocketClient client = RocketClientFactory.builder("https://api.example.com")
32 * .withCircuitBreaker(5, 30000) // 5 failures, 30s timeout
33 * .build();
34 * </code></pre>
35 *
36 * <h2>From Configuration</h2>
37 * <pre class="language-java"><code>
38 * RocketRestConfig config = RocketRestConfig.builder("https://api.example.com")
39 * .authStrategy(AuthStrategyFactory.createBearerToken("token"))
40 * .build();
41 *
42 * RocketClient client = RocketClientFactory.fromConfig(config)
43 * .withCircuitBreaker()
44 * .build();
45 * </code></pre>
46 *
47 * <h2>Async Client</h2>
48 * <pre class="language-java"><code>
49 * ExecutorService executor = Executors.newFixedThreadPool(4);
50 *
51 * AsyncHttpClient asyncClient = RocketClientFactory.builder("https://api.example.com")
52 * .withExecutorService(executor)
53 * .buildAsync();
54 * </code></pre>
55 *
56 * <h2>Fluent Client (Result Pattern)</h2>
57 * <pre class="language-java"><code>
58 * FluentHttpClient fluentClient = RocketClientFactory.builder("https://api.example.com")
59 * .buildFluent();
60 *
61 * Result<User, ApiError> result = fluentClient.executeWithResult(request);
62 * </code></pre>
63 *
64 * <h2>Custom Decorator</h2>
65 * <pre class="language-java"><code>
66 * RocketClient client = RocketClientFactory.builder("https://api.example.com")
67 * .withCircuitBreaker()
68 * .withCustomDecorator(base -> new LoggingClientDecorator(base))
69 * .build();
70 * </code></pre>
71 *
72 * <h2>With Interceptors</h2>
73 * <pre class="language-java"><code>
74 * // Add retry with exponential backoff
75 * RocketClient client = RocketClientFactory.builder("https://api.example.com")
76 * .withRetry(3, 1000) // 3 retries, 1s initial delay
77 * .build();
78 *
79 * // Multiple interceptors
80 * RocketClient client = RocketClientFactory.builder("https://api.example.com")
81 * .withInterceptor(new LoggingInterceptor())
82 * .withRetry(3, 1000, 2.0) // With exponential backoff
83 * .build();
84 * </code></pre>
85 *
86 * @author guinetik <guinetik@gmail.com>
87 * @see RocketClient
88 * @see CircuitBreakerClient
89 * @see FluentHttpClient
90 * @see RequestInterceptor
91 * @see RetryInterceptor
92 * @since 1.0.0
93 */
94 public class RocketClientFactory {
95
96 // Function type for client provider used in testing/mocking
97 private static UnaryOperator<RocketRestConfig> clientProvider = null;
98
99 /**
100 * Sets a custom client provider for testing/mocking.
101 * This allows tests to inject mock clients.
102 *
103 * @param provider The provider function
104 */
105 public static void setClientProvider(UnaryOperator<RocketRestConfig> provider) {
106 clientProvider = provider;
107 }
108
109 /**
110 * Resets the client provider to the default.
111 */
112 public static void resetToDefault() {
113 clientProvider = null;
114 }
115
116 /**
117 * Builder class for constructing RocketClient instances with various decorators.
118 */
119 public static class Builder {
120 private final String baseUrl;
121 private RocketRestOptions options;
122 private ExecutorService executorService;
123 private boolean enableCircuitBreaker = false;
124 private int failureThreshold = HttpConstants.CircuitBreaker.DEFAULT_FAILURE_THRESHOLD;
125 private long resetTimeoutMs = HttpConstants.CircuitBreaker.DEFAULT_RESET_TIMEOUT_MS;
126 private long failureDecayTimeMs = HttpConstants.CircuitBreaker.DEFAULT_FAILURE_DECAY_TIME_MS;
127 private CircuitBreakerClient.FailurePolicy failurePolicy = CircuitBreakerClient.FailurePolicy.ALL_EXCEPTIONS;
128 private Predicate<RocketRestException> failurePredicate = null;
129 private UnaryOperator<RocketClient> customDecorator = null;
130 private List<RequestInterceptor> interceptors = new ArrayList<RequestInterceptor>();
131 private int interceptorMaxRetries = 3;
132
133 private Builder(String baseUrl) {
134 this.baseUrl = baseUrl;
135 this.options = new RocketRestOptions();
136 }
137
138 /**
139 * Sets the client options.
140 *
141 * @param options The RocketRestOptions to use
142 * @return this builder instance
143 */
144 public Builder withOptions(RocketRestOptions options) {
145 this.options = options;
146 return this;
147 }
148
149 /**
150 * Sets the executor service for async operations.
151 *
152 * @param executorService The executor service to use
153 * @return this builder instance
154 */
155 public Builder withExecutorService(ExecutorService executorService) {
156 this.executorService = executorService;
157 return this;
158 }
159
160 /**
161 * Enables the circuit breaker pattern with default settings.
162 *
163 * @return this builder instance
164 */
165 public Builder withCircuitBreaker() {
166 this.enableCircuitBreaker = true;
167 return this;
168 }
169
170 /**
171 * Enables the circuit breaker pattern with custom settings.
172 *
173 * @param failureThreshold Number of failures before opening circuit
174 * @param resetTimeoutMs Timeout in ms before trying to close the circuit
175 * @return this builder instance
176 */
177 public Builder withCircuitBreaker(int failureThreshold, long resetTimeoutMs) {
178 this.enableCircuitBreaker = true;
179 this.failureThreshold = failureThreshold;
180 this.resetTimeoutMs = resetTimeoutMs;
181 return this;
182 }
183
184 /**
185 * Enables the circuit breaker pattern with fully customized settings.
186 *
187 * @param failureThreshold Number of failures before opening circuit
188 * @param resetTimeoutMs Timeout in ms before trying to close the circuit
189 * @param failureDecayTimeMs Time after which failure count starts to decay
190 * @param failurePolicy Strategy to determine what counts as a failure
191 * @return this builder instance
192 */
193 public Builder withCircuitBreaker(int failureThreshold, long resetTimeoutMs,
194 long failureDecayTimeMs, CircuitBreakerClient.FailurePolicy failurePolicy) {
195 this.enableCircuitBreaker = true;
196 this.failureThreshold = failureThreshold;
197 this.resetTimeoutMs = resetTimeoutMs;
198 this.failureDecayTimeMs = failureDecayTimeMs;
199 this.failurePolicy = failurePolicy;
200 return this;
201 }
202
203 /**
204 * Sets a custom failure predicate for the circuit breaker.
205 *
206 * @param failurePredicate Custom predicate to determine what counts as a failure
207 * @return this builder instance
208 */
209 public Builder withFailurePredicate(Predicate<RocketRestException> failurePredicate) {
210 this.failurePredicate = failurePredicate;
211 return this;
212 }
213
214 /**
215 * Adds a custom decorator function that will be applied to the client.
216 *
217 * @param decorator Function that takes a client and returns a decorated client
218 * @return this builder instance
219 */
220 public Builder withCustomDecorator(UnaryOperator<RocketClient> decorator) {
221 this.customDecorator = decorator;
222 return this;
223 }
224
225 /**
226 * Adds an interceptor to the client.
227 *
228 * <p>Interceptors are applied in order based on their {@link RequestInterceptor#getOrder()}.
229 * Lower order values run first for requests and last for responses.
230 *
231 * @param interceptor The interceptor to add
232 * @return this builder instance
233 * @see RequestInterceptor
234 */
235 public Builder withInterceptor(RequestInterceptor interceptor) {
236 if (interceptor != null) {
237 this.interceptors.add(interceptor);
238 }
239 return this;
240 }
241
242 /**
243 * Adds retry capability with default settings.
244 *
245 * <p>Uses 3 retries with 1 second initial delay, 2x exponential backoff,
246 * and 30 second maximum delay.
247 *
248 * @return this builder instance
249 */
250 public Builder withRetry() {
251 return withInterceptor(new RetryInterceptor());
252 }
253
254 /**
255 * Adds retry capability with custom retry count and delay.
256 *
257 * @param maxRetries Maximum number of retries
258 * @param initialDelayMs Initial delay between retries in milliseconds
259 * @return this builder instance
260 */
261 public Builder withRetry(int maxRetries, long initialDelayMs) {
262 return withInterceptor(new RetryInterceptor(maxRetries, initialDelayMs));
263 }
264
265 /**
266 * Adds retry capability with exponential backoff.
267 *
268 * @param maxRetries Maximum number of retries
269 * @param initialDelayMs Initial delay in milliseconds
270 * @param backoffMultiplier Multiplier for each retry (e.g., 2.0 doubles delay)
271 * @return this builder instance
272 */
273 public Builder withRetry(int maxRetries, long initialDelayMs, double backoffMultiplier) {
274 return withInterceptor(new RetryInterceptor(maxRetries, initialDelayMs, backoffMultiplier));
275 }
276
277 /**
278 * Sets the maximum number of retries allowed by the interceptor chain.
279 *
280 * <p>This is a global limit that applies across all retry interceptors.
281 * Default is 3.
282 *
283 * @param maxRetries Maximum retries for the interceptor chain
284 * @return this builder instance
285 */
286 public Builder withMaxRetries(int maxRetries) {
287 this.interceptorMaxRetries = maxRetries;
288 return this;
289 }
290
291 /**
292 * Builds a synchronous RocketClient with the configured settings.
293 *
294 * @return A new RocketClient instance
295 */
296 public RocketClient build() {
297 // First check if there's a custom client provider for testing
298 if (clientProvider != null) {
299 RocketRestConfig config = new RocketRestConfig.Builder(baseUrl)
300 .defaultOptions(o -> {
301 for (String key : options.getKeys()) {
302 o.set(key, options.getRaw(key));
303 }
304 })
305 .build();
306 return (RocketClient) clientProvider.apply(config);
307 }
308 // Check if the options have circuit breaker enabled
309 boolean circuitBreakerEnabled = this.enableCircuitBreaker ||
310 options.getBoolean(HttpConstants.CircuitBreaker.CIRCUIT_BREAKER_ENABLED, false);
311 // Create base client
312 RocketClient client = new DefaultHttpClient(baseUrl, options);
313
314 // Apply circuit breaker if enabled in builder or options
315 if (circuitBreakerEnabled) {
316 // Get circuit breaker settings from options if not specified in builder
317 int threshold = this.enableCircuitBreaker ?
318 this.failureThreshold :
319 options.getInt(HttpConstants.CircuitBreaker.CIRCUIT_BREAKER_FAILURE_THRESHOLD,
320 HttpConstants.CircuitBreaker.DEFAULT_FAILURE_THRESHOLD);
321 //
322 long timeout = this.enableCircuitBreaker ?
323 this.resetTimeoutMs :
324 options.getLong(HttpConstants.CircuitBreaker.CIRCUIT_BREAKER_RESET_TIMEOUT_MS,
325 HttpConstants.CircuitBreaker.DEFAULT_RESET_TIMEOUT_MS);
326 // Determine failure policy
327 CircuitBreakerClient.FailurePolicy policy = this.failurePolicy;
328 if (!this.enableCircuitBreaker && options.contains(HttpConstants.CircuitBreaker.CIRCUIT_BREAKER_FAILURE_POLICY)) {
329 String policyStr = options.getString(HttpConstants.CircuitBreaker.CIRCUIT_BREAKER_FAILURE_POLICY, null);
330 if (HttpConstants.CircuitBreaker.CIRCUIT_BREAKER_POLICY_SERVER_ONLY.equals(policyStr)) {
331 policy = CircuitBreakerClient.FailurePolicy.SERVER_ERRORS_ONLY;
332 }
333 }
334 // Apply circuit breaker
335 client = new CircuitBreakerClient(
336 client,
337 threshold,
338 timeout,
339 this.failureDecayTimeMs,
340 policy,
341 this.failurePredicate
342 );
343 }
344 // Apply interceptors if any are configured
345 if (!interceptors.isEmpty()) {
346 client = new InterceptingClient(client, interceptors, interceptorMaxRetries);
347 }
348
349 // Apply any custom decorator
350 if (customDecorator != null) {
351 client = customDecorator.apply(client);
352 }
353 // Return the client
354 return client;
355 }
356
357 /**
358 * Builds a fluent HTTP client with the configured settings.
359 * This client uses the Result pattern instead of exceptions.
360 *
361 * @return A new FluentHttpClient instance
362 */
363 public FluentHttpClient buildFluent() {
364 RocketClient client = build();
365 return new FluentHttpClient(client, baseUrl, options);
366 }
367
368 /**
369 * Builds an asynchronous RocketClient with the configured settings.
370 *
371 * @return A new AsyncHttpClient instance
372 * @throws IllegalStateException if no executor service was provided
373 */
374 public AsyncHttpClient buildAsync() {
375 if (executorService == null) {
376 throw new IllegalStateException("ExecutorService must be provided for async client");
377 }
378
379 RocketClient client = build();
380 return new AsyncHttpClient(client, executorService);
381 }
382 }
383
384 /**
385 * Creates a builder for constructing RocketClient instances.
386 *
387 * @param baseUrl The base URL for the client
388 * @return A new builder instance
389 */
390 public static Builder builder(String baseUrl) {
391 return new Builder(baseUrl);
392 }
393
394 /**
395 * Creates a builder from an existing RocketRestConfig.
396 *
397 * @param config The RocketRestConfig to use
398 * @return A new builder instance pre-configured with settings from the config
399 */
400 public static Builder fromConfig(RocketRestConfig config) {
401 return builder(config.getServiceUrl())
402 .withOptions(config.getDefaultOptions());
403 }
404
405 /**
406 * Creates a default HTTP client with the given config.
407 *
408 * @param config The RocketRestConfig to use
409 * @return A new DefaultHttpClient instance
410 */
411 public static RocketClient createDefaultClient(RocketRestConfig config) {
412 return builder(config.getServiceUrl())
413 .withOptions(config.getDefaultOptions())
414 .build();
415 }
416
417 /**
418 * Creates a fluent HTTP client with the given config.
419 * This client uses the Result pattern instead of exceptions.
420 *
421 * @param config The RocketRestConfig to use
422 * @return A new FluentHttpClient instance
423 */
424 public static FluentHttpClient createFluentClient(RocketRestConfig config) {
425 return new FluentHttpClient(config.getServiceUrl(), config.getDefaultOptions());
426 }
427
428 /**
429 * Creates a fluent HTTP client with the given base URL.
430 * This client uses the Result pattern instead of exceptions.
431 *
432 * @param baseUrl The base URL for the client
433 * @return A new FluentHttpClient instance
434 */
435 public static FluentHttpClient createFluentClient(String baseUrl) {
436 return new FluentHttpClient(baseUrl);
437 }
438 }