View Javadoc
1   package com.guinetik.rr.interceptor;
2   
3   import com.guinetik.rr.http.RocketClient;
4   import com.guinetik.rr.http.RocketRestException;
5   import com.guinetik.rr.request.RequestSpec;
6   import org.slf4j.Logger;
7   import org.slf4j.LoggerFactory;
8   
9   import javax.net.ssl.SSLContext;
10  import java.util.ArrayList;
11  import java.util.Collections;
12  import java.util.Comparator;
13  import java.util.List;
14  
15  /**
16   * Decorator that adds interceptor chain support to any {@link RocketClient}.
17   *
18   * <p>This client wraps another RocketClient and applies a chain of interceptors
19   * to every request. Interceptors can modify requests, transform responses,
20   * and handle errors including retry logic.
21   *
22   * <h2>Interceptor Execution Order</h2>
23   * <pre>
24   * Request Flow:
25   *   beforeRequest(Interceptor 1) → beforeRequest(Interceptor 2) → ... → delegate.execute()
26   *
27   * Response Flow:
28   *   delegate.execute() → ... → afterResponse(Interceptor 2) → afterResponse(Interceptor 1)
29   *
30   * Error Flow:
31   *   exception → onError(Interceptor 1) → onError(Interceptor 2) → ...
32   * </pre>
33   *
34   * <h2>Usage</h2>
35   * <pre class="language-java"><code>
36   * RocketClient baseClient = new DefaultHttpClient("https://api.example.com");
37   *
38   * List&lt;RequestInterceptor&gt; interceptors = new ArrayList&lt;&gt;();
39   * interceptors.add(new LoggingInterceptor());
40   * interceptors.add(new RetryInterceptor(3, 1000));
41   *
42   * RocketClient client = new InterceptingClient(baseClient, interceptors);
43   * </code></pre>
44   *
45   * @author guinetik &lt;guinetik@gmail.com&gt;
46   * @see RequestInterceptor
47   * @see RocketClient
48   * @since 1.1.0
49   */
50  public class InterceptingClient implements RocketClient {
51  
52      private static final Logger logger = LoggerFactory.getLogger(InterceptingClient.class);
53  
54      private final RocketClient delegate;
55      private final List<RequestInterceptor> interceptors;
56      private final int maxRetries;
57  
58      /**
59       * Creates an intercepting client with the given interceptors.
60       *
61       * @param delegate The underlying client to wrap
62       * @param interceptors The interceptors to apply (will be sorted by order)
63       */
64      public InterceptingClient(RocketClient delegate, List<RequestInterceptor> interceptors) {
65          this(delegate, interceptors, 3);
66      }
67  
68      /**
69       * Creates an intercepting client with the given interceptors and retry limit.
70       *
71       * @param delegate The underlying client to wrap
72       * @param interceptors The interceptors to apply (will be sorted by order)
73       * @param maxRetries The maximum number of retries allowed
74       */
75      public InterceptingClient(RocketClient delegate, List<RequestInterceptor> interceptors, int maxRetries) {
76          if (delegate == null) {
77              throw new NullPointerException("delegate must not be null");
78          }
79          this.delegate = delegate;
80          this.maxRetries = maxRetries;
81  
82          // Sort interceptors by order and make immutable copy
83          List<RequestInterceptor> sorted = new ArrayList<RequestInterceptor>(
84              interceptors != null ? interceptors : Collections.<RequestInterceptor>emptyList()
85          );
86          Collections.sort(sorted, new Comparator<RequestInterceptor>() {
87              @Override
88              public int compare(RequestInterceptor a, RequestInterceptor b) {
89                  return Integer.compare(a.getOrder(), b.getOrder());
90              }
91          });
92          this.interceptors = Collections.unmodifiableList(sorted);
93      }
94  
95      @Override
96      public <Req, Res> Res execute(RequestSpec<Req, Res> requestSpec) throws RocketRestException {
97          return executeWithRetry(requestSpec, 0);
98      }
99  
100     /**
101      * Executes the request with retry support.
102      */
103     private <Req, Res> Res executeWithRetry(RequestSpec<Req, Res> requestSpec, int retryCount)
104             throws RocketRestException {
105 
106         final int currentRetry = retryCount;
107 
108         // Create chain context for this execution
109         InterceptorChain chain = new InterceptorChain() {
110             @Override
111             public <R, S> S retry(RequestSpec<R, S> request) throws RocketRestException {
112                 if (currentRetry >= maxRetries) {
113                     throw new RocketRestException("Maximum retry count exceeded: " + maxRetries);
114                 }
115                 logger.debug("Retrying request (attempt {}): {} {}",
116                     currentRetry + 1, request.getMethod(), request.getEndpoint());
117                 return executeWithRetry(request, currentRetry + 1);
118             }
119 
120             @Override
121             public int getRetryCount() {
122                 return currentRetry;
123             }
124 
125             @Override
126             public int getMaxRetries() {
127                 return maxRetries;
128             }
129         };
130 
131         // Apply beforeRequest interceptors (in order)
132         RequestSpec<Req, Res> currentRequest = requestSpec;
133         for (RequestInterceptor interceptor : interceptors) {
134             currentRequest = interceptor.beforeRequest(currentRequest);
135         }
136 
137         try {
138             // Execute the actual request
139             Res response = delegate.execute(currentRequest);
140 
141             // Apply afterResponse interceptors (in reverse order)
142             for (int i = interceptors.size() - 1; i >= 0; i--) {
143                 response = interceptors.get(i).afterResponse(response, currentRequest);
144             }
145 
146             return response;
147 
148         } catch (RocketRestException e) {
149             // Apply onError interceptors (in order) - first one to return wins
150             for (RequestInterceptor interceptor : interceptors) {
151                 try {
152                     Res recovered = interceptor.onError(e, currentRequest, chain);
153                     // If we get here, the interceptor recovered - apply afterResponse
154                     for (int i = interceptors.size() - 1; i >= 0; i--) {
155                         recovered = interceptors.get(i).afterResponse(recovered, currentRequest);
156                     }
157                     return recovered;
158                 } catch (RocketRestException rethrown) {
159                     // Interceptor rethrew (possibly modified) exception, continue to next
160                     e = rethrown;
161                 }
162             }
163             // All interceptors rethrew, propagate the last exception
164             throw e;
165         }
166     }
167 
168     @Override
169     public void configureSsl(SSLContext sslContext) {
170         delegate.configureSsl(sslContext);
171     }
172 
173     @Override
174     public void setBaseUrl(String baseUrl) {
175         delegate.setBaseUrl(baseUrl);
176     }
177 
178     /**
179      * Gets the list of interceptors in execution order.
180      *
181      * @return Unmodifiable list of interceptors
182      */
183     public List<RequestInterceptor> getInterceptors() {
184         return interceptors;
185     }
186 }