View Javadoc
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&lt;User, ApiError&gt; 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 -&gt; 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 &lt;guinetik@gmail.com&gt;
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 }