View Javadoc
1   package com.guinetik.rr;
2   
3   import com.guinetik.rr.api.AsyncApiClient;
4   import com.guinetik.rr.api.DefaultApiClient;
5   import com.guinetik.rr.api.FluentApiClient;
6   import com.guinetik.rr.auth.AbstractOAuth2Strategy;
7   import com.guinetik.rr.request.RequestBuilder;
8   import com.guinetik.rr.request.RequestSpec;
9   import com.guinetik.rr.result.ApiError;
10  import com.guinetik.rr.result.Result;
11  import org.slf4j.Logger;
12  import org.slf4j.LoggerFactory;
13  
14  import java.util.Date;
15  import java.util.Map;
16  import java.util.concurrent.CompletableFuture;
17  import java.util.concurrent.ExecutorService;
18  import java.util.concurrent.Executors;
19  
20  /**
21   * Main entry point for interacting with REST APIs using RocketRest.
22   *
23   * <p>This class provides a unified facade for making HTTP requests with three different API styles:
24   * <ul>
25   *   <li><b>Synchronous API</b> - Traditional blocking calls via {@link #sync()}</li>
26   *   <li><b>Asynchronous API</b> - Non-blocking calls with {@link CompletableFuture} via {@link #async()}</li>
27   *   <li><b>Fluent API</b> - Functional error handling with {@link Result} pattern via {@link #fluent()}</li>
28   * </ul>
29   *
30   * <h2>Quick Start</h2>
31   * <pre class="language-java"><code>
32   * // Create a client with simple URL
33   * RocketRest client = new RocketRest("https://api.example.com");
34   *
35   * // Make a GET request
36   * User user = client.get("/users/1", User.class);
37   *
38   * // Don't forget to shutdown when done
39   * client.shutdown();
40   * </code></pre>
41   *
42   * <h2>Using Different API Styles</h2>
43   * <pre class="language-java"><code>
44   * // Synchronous API - blocks until response
45   * Todo todo = client.sync().get("/todos/1", Todo.class);
46   *
47   * // Asynchronous API - returns CompletableFuture
48   * CompletableFuture&lt;Todo&gt; future = client.async().get("/todos/1", Todo.class);
49   * future.thenAccept(t -&gt; System.out.println(t.getTitle()));
50   *
51   * // Fluent API with Result pattern - no exceptions
52   * Result&lt;Todo, ApiError&gt; result = client.fluent().get("/todos/1", Todo.class);
53   * result.match(
54   *     todo -&gt; System.out.println("Success: " + todo.getTitle()),
55   *     error -&gt; System.err.println("Error: " + error.getMessage())
56   * );
57   * </code></pre>
58   *
59   * <h2>Configuration</h2>
60   * <pre class="language-java"><code>
61   * RocketRestConfig config = RocketRestConfig.builder("https://api.example.com")
62   *     .authStrategy(AuthStrategyFactory.createBearerToken("my-token"))
63   *     .defaultOptions(opts -&gt; {
64   *         opts.set(RocketRestOptions.RETRY_ENABLED, true);
65   *         opts.set(RocketRestOptions.MAX_RETRIES, 3);
66   *     })
67   *     .build();
68   *
69   * RocketRest client = new RocketRest(config);
70   * </code></pre>
71   *
72   * @author guinetik &lt;guinetik@gmail.com&gt;
73   * @see RocketRestConfig
74   * @see Result
75   * @since 1.0.0
76   */
77  public class RocketRest {
78  
79      protected final Logger logger = LoggerFactory.getLogger(this.getClass());
80  
81      private final RocketRestOptions options;
82      private final DefaultApiClient syncClient;
83      private final AsyncApiClient asyncClient;
84      private final FluentApiClient fluentClient;
85      
86      /**
87       * Interface for synchronous API operations.
88       */
89      public interface SyncApi {
90          <T> T get(String endpoint, Class<T> responseType);
91          <T> T get(String endpoint, Class<T> responseType, Map<String, String> queryParams);
92          <Res> Res post(String endpoint, Class<Res> responseType);
93          <Req, Res> Res post(String endpoint, Req body, Class<Res> responseType);
94          <Res> Res put(String endpoint, Class<Res> responseType);
95          <Req, Res> Res put(String endpoint, Req body, Class<Res> responseType);
96          <T> T delete(String endpoint, Class<T> responseType);
97          <Req, Res> Res execute(RequestSpec<Req, Res> requestSpec);
98      }
99  
100     /**
101      * Interface for asynchronous API operations.
102      */
103     public interface AsyncApi {
104         <T> CompletableFuture<T> get(String endpoint, Class<T> responseType);
105         <T> CompletableFuture<T> get(String endpoint, Class<T> responseType, Map<String, String> queryParams);
106         <Res> CompletableFuture<Res> post(String endpoint, Class<Res> responseType);
107         <Req, Res> CompletableFuture<Res> post(String endpoint, Req body, Class<Res> responseType);
108         <Res> CompletableFuture<Res> put(String endpoint, Class<Res> responseType);
109         <Req, Res> CompletableFuture<Res> put(String endpoint, Req body, Class<Res> responseType);
110         <T> CompletableFuture<T> delete(String endpoint, Class<T> responseType);
111         <Req, Res> CompletableFuture<Res> execute(RequestSpec<Req, Res> requestSpec);
112         void shutdown();
113     }
114 
115     /**
116      * Interface for fluent API operations with a Result pattern.
117      */
118     public interface FluentApi {
119         <T> Result<T, ApiError> get(String endpoint, Class<T> responseType);
120         <T> Result<T, ApiError> get(String endpoint, Class<T> responseType, Map<String, String> queryParams);
121         <Res> Result<Res, ApiError> post(String endpoint, Class<Res> responseType);
122         <Req, Res> Result<Res, ApiError> post(String endpoint, Req body, Class<Res> responseType);
123         <Res> Result<Res, ApiError> put(String endpoint, Class<Res> responseType);
124         <Req, Res> Result<Res, ApiError> put(String endpoint, Req body, Class<Res> responseType);
125         <T> Result<T, ApiError> delete(String endpoint, Class<T> responseType);
126         <Req, Res> Result<Res, ApiError> execute(RequestSpec<Req, Res> requestSpec);
127     }
128 
129     private RocketRestConfig config;
130 
131     /**
132      * Creates a new {@link RocketRest} instance.
133      *
134      * @param config the configuration for the REST client.
135      */
136     public RocketRest(RocketRestConfig config) {
137         this(config.getServiceUrl(), config);
138     }
139 
140     /**
141      * Creates a new {@link RocketRest} instance with a specific base URL.
142      *
143      * @param baseUrl the base URL of the API.
144      * @param config  the configuration for the REST client.
145      */
146     public RocketRest(String baseUrl, RocketRestConfig config) {
147         // Initialize options from config's default options
148         this.config = config;
149         if (this.config != null && this.config.getDefaultOptions() != null) {
150             this.options = new RocketRestOptions();
151             RocketRestOptions defaultOptions = this.config.getDefaultOptions();
152 
153             // Copy all default options to this client's options
154             for (String key : defaultOptions.getKeys()) {
155                 Object value = defaultOptions.getRaw(key);
156                 if (value != null) {
157                     this.options.set(key, value);
158                 }
159             }
160         } else {
161             this.options = new RocketRestOptions();
162         }
163 
164         // Initialize clients (they will get default options from config)
165         this.syncClient = new DefaultApiClient(baseUrl, this.config);
166 
167         // Get the pool size from options
168         int poolSize = options.getInt(RocketRestOptions.ASYNC_POOL_SIZE, 4);
169         ExecutorService asyncExecutor = Executors.newFixedThreadPool(poolSize);
170         this.asyncClient = new AsyncApiClient(baseUrl, this.config, asyncExecutor);
171         
172         // Initialize the fluent client
173         this.fluentClient = new FluentApiClient(baseUrl, this.config);
174 
175         logger.info("Initialized RocketRest with base URL: {} and async pool size: {}", baseUrl, poolSize);
176     }
177 
178     /**
179      * Performs a synchronous GET request to the specified endpoint.
180      *
181      * @param <T> The response type
182      * @param endpoint The API endpoint
183      * @param responseType The class of the response type
184      * @return The response object
185      */
186     public <T> T get(String endpoint, Class<T> responseType) {
187         return sync().get(endpoint, responseType);
188     }
189 
190     /**
191      * Performs a synchronous GET request with query parameters.
192      *
193      * @param <T> The response type
194      * @param endpoint The API endpoint
195      * @param responseType The class of the response type
196      * @param queryParams The query parameters
197      * @return The response object
198      */
199     public <T> T get(String endpoint, Class<T> responseType, Map<String, String> queryParams) {
200         return sync().get(endpoint, responseType, queryParams);
201     }
202 
203     /**
204      * Performs a synchronous POST request.
205      *
206      * @param <Res> The response type
207      * @param endpoint The API endpoint
208      * @param responseType The class of the response type
209      * @return The response object
210      */
211     public <Res> Res post(String endpoint, Class<Res> responseType) {
212         return sync().post(endpoint, responseType);
213     }
214 
215     /**
216      * Performs a synchronous POST request with a body.
217      *
218      * @param <Req> The request body type
219      * @param <Res> The response type
220      * @param endpoint The API endpoint
221      * @param body The request body
222      * @param responseType The class of the response type
223      * @return The response object
224      */
225     public <Req, Res> Res post(String endpoint, Req body, Class<Res> responseType) {
226         return sync().post(endpoint, body, responseType);
227     }
228 
229     /**
230      * Performs a synchronous PUT request.
231      *
232      * @param <Res> The response type
233      * @param endpoint The API endpoint
234      * @param responseType The class of the response type
235      * @return The response object
236      */
237     public <Res> Res put(String endpoint, Class<Res> responseType) {
238         return sync().put(endpoint, responseType);
239     }
240 
241     /**
242      * Performs a synchronous PUT request with a body.
243      *
244      * @param <Req> The request body type
245      * @param <Res> The response type
246      * @param endpoint The API endpoint
247      * @param body The request body
248      * @param responseType The class of the response type
249      * @return The response object
250      */
251     public <Req, Res> Res put(String endpoint, Req body, Class<Res> responseType) {
252         return sync().put(endpoint, body, responseType);
253     }
254 
255     /**
256      * Performs a synchronous DELETE request.
257      *
258      * @param <T> The response type
259      * @param endpoint The API endpoint
260      * @param responseType The class of the response type
261      * @return The response object
262      */
263     public <T> T delete(String endpoint, Class<T> responseType) {
264         return sync().delete(endpoint, responseType);
265     }
266 
267     /**
268      * Executes a synchronous request with the given request specification.
269      *
270      * @param <Req> The request body type
271      * @param <Res> The response type
272      * @param requestSpec The request specification
273      * @return The response object
274      */
275     public <Req, Res> Res execute(RequestSpec<Req, Res> requestSpec) {
276         return sync().execute(requestSpec);
277     }
278 
279     /**
280      * Gets the synchronous API interface.
281      * 
282      * @return The synchronous API interface
283      */
284     public SyncApi sync() {
285         return new SyncApiImpl();
286     }
287     
288     /**
289      * Gets the asynchronous API interface.
290      * 
291      * @return The asynchronous API interface
292      */
293     public AsyncApi async() {
294         return new AsyncApiImpl();
295     }
296     
297     /**
298      * Gets the fluent API interface with Result pattern.
299      * 
300      * @return The fluent API interface
301      */
302     public FluentApi fluent() {
303         return new FluentApiImpl();
304     }
305 
306     /**
307      * Configures a client option with the specified value.
308      *
309      * @param key   The option key from ClientOptions.
310      * @param value The option value.
311      * @return this client instance for method chaining
312      */
313     public RocketRest configure(String key, Object value) {
314         options.set(key, value);
315 
316         // Apply to all clients
317         syncClient.configure(key, value);
318         asyncClient.configure(key, value);
319         fluentClient.configure(key, value);
320 
321         // Special handling for async pool size
322         if (RocketRestOptions.ASYNC_POOL_SIZE.equals(key) && value instanceof Integer) {
323             logger.info("Note: Changing ASYNC_POOL_SIZE after initialization is not supported");
324         }
325 
326         return this;
327     }
328 
329     /**
330      * Shuts down all resources used by this client.
331      */
332     public void shutdown() {
333         asyncClient.shutdown();
334         logger.info("RocketRest shutdown completed.");
335     }
336     
337     // Helper methods for building request specs
338     
339     /**
340      * Creates a GET request specification.
341      *
342      * @param <T>          The response type
343      * @param endpoint     The API endpoint
344      * @param responseType The class of the response type
345      * @return A built request specification
346      */
347     private <T> RequestSpec<Void, T> createGetRequest(String endpoint, Class<T> responseType) {
348         logger.debug("Creating GET request to endpoint: {}", endpoint);
349         return new RequestBuilder<Void, T>()
350                 .endpoint(endpoint)
351                 .method("GET")
352                 .responseType(responseType)
353                 .build();
354     }
355     
356     /**
357      * Creates a GET request specification with query parameters.
358      *
359      * @param <T>          The response type
360      * @param endpoint     The API endpoint
361      * @param responseType The class of the response type
362      * @param queryParams  The query parameters
363      * @return A built request specification
364      */
365     private <T> RequestSpec<Void, T> createGetRequest(String endpoint, Class<T> responseType, Map<String, String> queryParams) {
366         logger.debug("Creating GET request to endpoint: {} with params: {}", endpoint, queryParams);
367         return new RequestBuilder<Void, T>()
368                 .endpoint(endpoint)
369                 .method("GET")
370                 .queryParams(queryParams)
371                 .responseType(responseType)
372                 .build();
373     }
374     
375     /**
376      * Creates a POST request specification.
377      *
378      * @param <Res>        The response type
379      * @param endpoint     The API endpoint
380      * @param responseType The class of the response type
381      * @return A built request specification
382      */
383     private <Res> RequestSpec<Void, Res> createPostRequest(String endpoint, Class<Res> responseType) {
384         logger.debug("Creating POST request to endpoint: {}", endpoint);
385         return new RequestBuilder<Void, Res>()
386                 .endpoint(endpoint)
387                 .method("POST")
388                 .responseType(responseType)
389                 .build();
390     }
391     
392     /**
393      * Creates a POST request specification with a body.
394      *
395      * @param <Req>        The request body type
396      * @param <Res>        The response type
397      * @param endpoint     The API endpoint
398      * @param body         The request body
399      * @param responseType The class of the response type
400      * @return A built request specification
401      */
402     private <Req, Res> RequestSpec<Req, Res> createPostRequest(String endpoint, Req body, Class<Res> responseType) {
403         logger.debug("Creating POST request to endpoint: {} with body", endpoint);
404         return new RequestBuilder<Req, Res>()
405                 .endpoint(endpoint)
406                 .method("POST")
407                 .body(body)
408                 .responseType(responseType)
409                 .build();
410     }
411     
412     /**
413      * Creates a PUT request specification.
414      *
415      * @param <Res>        The response type
416      * @param endpoint     The API endpoint
417      * @param responseType The class of the response type
418      * @return A built request specification
419      */
420     private <Res> RequestSpec<Void, Res> createPutRequest(String endpoint, Class<Res> responseType) {
421         logger.debug("Creating PUT request to endpoint: {}", endpoint);
422         return new RequestBuilder<Void, Res>()
423                 .endpoint(endpoint)
424                 .method("PUT")
425                 .responseType(responseType)
426                 .build();
427     }
428     
429     /**
430      * Creates a PUT request specification with a body.
431      *
432      * @param <Req>        The request body type
433      * @param <Res>        The response type
434      * @param endpoint     The API endpoint
435      * @param body         The request body
436      * @param responseType The class of the response type
437      * @return A built request specification
438      */
439     private <Req, Res> RequestSpec<Req, Res> createPutRequest(String endpoint, Req body, Class<Res> responseType) {
440         logger.debug("Creating PUT request to endpoint: {} with body", endpoint);
441         return new RequestBuilder<Req, Res>()
442                 .endpoint(endpoint)
443                 .method("PUT")
444                 .body(body)
445                 .responseType(responseType)
446                 .build();
447     }
448     
449     /**
450      * Creates a DELETE request specification.
451      *
452      * @param <T>          The response type
453      * @param endpoint     The API endpoint
454      * @param responseType The class of the response type
455      * @return A built request specification
456      */
457     private <T> RequestSpec<Void, T> createDeleteRequest(String endpoint, Class<T> responseType) {
458         logger.debug("Creating DELETE request to endpoint: {}", endpoint);
459         return new RequestBuilder<Void, T>()
460                 .endpoint(endpoint)
461                 .method("DELETE")
462                 .responseType(responseType)
463                 .build();
464     }
465     
466     // Inner class implementations of the API interfaces
467     
468     /**
469      * Implementation of SyncApi that delegates to the underlying DefaultApiClient.
470      */
471     private class SyncApiImpl implements SyncApi {
472         @Override
473         public <T> T get(String endpoint, Class<T> responseType) {
474             return syncClient.execute(createGetRequest(endpoint, responseType));
475         }
476         
477         @Override
478         public <T> T get(String endpoint, Class<T> responseType, Map<String, String> queryParams) {
479             return syncClient.execute(createGetRequest(endpoint, responseType, queryParams));
480         }
481         
482         @Override
483         public <Res> Res post(String endpoint, Class<Res> responseType) {
484             return syncClient.execute(createPostRequest(endpoint, responseType));
485         }
486         
487         @Override
488         public <Req, Res> Res post(String endpoint, Req body, Class<Res> responseType) {
489             return syncClient.execute(createPostRequest(endpoint, body, responseType));
490         }
491         
492         @Override
493         public <Res> Res put(String endpoint, Class<Res> responseType) {
494             return syncClient.execute(createPutRequest(endpoint, responseType));
495         }
496         
497         @Override
498         public <Req, Res> Res put(String endpoint, Req body, Class<Res> responseType) {
499             return syncClient.execute(createPutRequest(endpoint, body, responseType));
500         }
501         
502         @Override
503         public <T> T delete(String endpoint, Class<T> responseType) {
504             return syncClient.execute(createDeleteRequest(endpoint, responseType));
505         }
506         
507         @Override
508         public <Req, Res> Res execute(RequestSpec<Req, Res> requestSpec) {
509             return syncClient.execute(requestSpec);
510         }
511     }
512     
513     /**
514      * Implementation of AsyncApi that delegates to the underlying AsyncApiClient.
515      */
516     private class AsyncApiImpl implements AsyncApi {
517         @Override
518         public <T> CompletableFuture<T> get(String endpoint, Class<T> responseType) {
519             return asyncClient.executeAsync(createGetRequest(endpoint, responseType));
520         }
521         
522         @Override
523         public <T> CompletableFuture<T> get(String endpoint, Class<T> responseType, Map<String, String> queryParams) {
524             return asyncClient.executeAsync(createGetRequest(endpoint, responseType, queryParams));
525         }
526         
527         @Override
528         public <Res> CompletableFuture<Res> post(String endpoint, Class<Res> responseType) {
529             return asyncClient.executeAsync(createPostRequest(endpoint, responseType));
530         }
531         
532         @Override
533         public <Req, Res> CompletableFuture<Res> post(String endpoint, Req body, Class<Res> responseType) {
534             return asyncClient.executeAsync(createPostRequest(endpoint, body, responseType));
535         }
536         
537         @Override
538         public <Res> CompletableFuture<Res> put(String endpoint, Class<Res> responseType) {
539             return asyncClient.executeAsync(createPutRequest(endpoint, responseType));
540         }
541         
542         @Override
543         public <Req, Res> CompletableFuture<Res> put(String endpoint, Req body, Class<Res> responseType) {
544             return asyncClient.executeAsync(createPutRequest(endpoint, body, responseType));
545         }
546         
547         @Override
548         public <T> CompletableFuture<T> delete(String endpoint, Class<T> responseType) {
549             return asyncClient.executeAsync(createDeleteRequest(endpoint, responseType));
550         }
551         
552         @Override
553         public <Req, Res> CompletableFuture<Res> execute(RequestSpec<Req, Res> requestSpec) {
554             return asyncClient.executeAsync(requestSpec);
555         }
556         
557         @Override
558         public void shutdown() {
559             asyncClient.shutdown();
560         }
561     }
562     
563     /**
564      * Implementation of FluentApi that delegates to the underlying FluentApiClient.
565      */
566     private class FluentApiImpl implements FluentApi {
567         @Override
568         public <T> Result<T, ApiError> get(String endpoint, Class<T> responseType) {
569             return fluentClient.executeWithResult(createGetRequest(endpoint, responseType));
570         }
571         
572         @Override
573         public <T> Result<T, ApiError> get(String endpoint, Class<T> responseType, Map<String, String> queryParams) {
574             return fluentClient.executeWithResult(createGetRequest(endpoint, responseType, queryParams));
575         }
576         
577         @Override
578         public <Res> Result<Res, ApiError> post(String endpoint, Class<Res> responseType) {
579             return fluentClient.executeWithResult(createPostRequest(endpoint, responseType));
580         }
581         
582         @Override
583         public <Req, Res> Result<Res, ApiError> post(String endpoint, Req body, Class<Res> responseType) {
584             return fluentClient.executeWithResult(createPostRequest(endpoint, body, responseType));
585         }
586         
587         @Override
588         public <Res> Result<Res, ApiError> put(String endpoint, Class<Res> responseType) {
589             return fluentClient.executeWithResult(createPutRequest(endpoint, responseType));
590         }
591         
592         @Override
593         public <Req, Res> Result<Res, ApiError> put(String endpoint, Req body, Class<Res> responseType) {
594             return fluentClient.executeWithResult(createPutRequest(endpoint, body, responseType));
595         }
596         
597         @Override
598         public <T> Result<T, ApiError> delete(String endpoint, Class<T> responseType) {
599             return fluentClient.executeWithResult(createDeleteRequest(endpoint, responseType));
600         }
601         
602         @Override
603         public <Req, Res> Result<Res, ApiError> execute(RequestSpec<Req, Res> requestSpec) {
604             return fluentClient.<Req, Res>executeWithResult(requestSpec);
605         }
606     }
607 
608     public void setBaseUrl(String baseUrl) {
609         this.syncClient.setBaseUrl(baseUrl);
610         this.asyncClient.setBaseUrl(baseUrl);
611         this.fluentClient.setBaseUrl(baseUrl);
612     }
613 
614     public String getAccessToken() {
615         if(this.config.getAuthStrategy() instanceof AbstractOAuth2Strategy) {
616             AbstractOAuth2Strategy strat = (AbstractOAuth2Strategy) this.config.getAuthStrategy();
617             return strat.getAccessToken();
618         }
619         return null;
620     }
621 
622     public Date getTokenExpiryTime() {
623         if(this.config.getAuthStrategy() instanceof AbstractOAuth2Strategy) {
624             AbstractOAuth2Strategy strat = (AbstractOAuth2Strategy) this.config.getAuthStrategy();
625             return strat.getTokenExpiryTime();
626         }
627         return null;
628     }
629 }