InterceptingClient.java
package com.guinetik.rr.interceptor;
import com.guinetik.rr.http.RocketClient;
import com.guinetik.rr.http.RocketRestException;
import com.guinetik.rr.request.RequestSpec;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.SSLContext;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* Decorator that adds interceptor chain support to any {@link RocketClient}.
*
* <p>This client wraps another RocketClient and applies a chain of interceptors
* to every request. Interceptors can modify requests, transform responses,
* and handle errors including retry logic.
*
* <h2>Interceptor Execution Order</h2>
* <pre>
* Request Flow:
* beforeRequest(Interceptor 1) → beforeRequest(Interceptor 2) → ... → delegate.execute()
*
* Response Flow:
* delegate.execute() → ... → afterResponse(Interceptor 2) → afterResponse(Interceptor 1)
*
* Error Flow:
* exception → onError(Interceptor 1) → onError(Interceptor 2) → ...
* </pre>
*
* <h2>Usage</h2>
* <pre class="language-java"><code>
* RocketClient baseClient = new DefaultHttpClient("https://api.example.com");
*
* List<RequestInterceptor> interceptors = new ArrayList<>();
* interceptors.add(new LoggingInterceptor());
* interceptors.add(new RetryInterceptor(3, 1000));
*
* RocketClient client = new InterceptingClient(baseClient, interceptors);
* </code></pre>
*
* @author guinetik <guinetik@gmail.com>
* @see RequestInterceptor
* @see RocketClient
* @since 1.1.0
*/
public class InterceptingClient implements RocketClient {
private static final Logger logger = LoggerFactory.getLogger(InterceptingClient.class);
private final RocketClient delegate;
private final List<RequestInterceptor> interceptors;
private final int maxRetries;
/**
* Creates an intercepting client with the given interceptors.
*
* @param delegate The underlying client to wrap
* @param interceptors The interceptors to apply (will be sorted by order)
*/
public InterceptingClient(RocketClient delegate, List<RequestInterceptor> interceptors) {
this(delegate, interceptors, 3);
}
/**
* Creates an intercepting client with the given interceptors and retry limit.
*
* @param delegate The underlying client to wrap
* @param interceptors The interceptors to apply (will be sorted by order)
* @param maxRetries The maximum number of retries allowed
*/
public InterceptingClient(RocketClient delegate, List<RequestInterceptor> interceptors, int maxRetries) {
if (delegate == null) {
throw new NullPointerException("delegate must not be null");
}
this.delegate = delegate;
this.maxRetries = maxRetries;
// Sort interceptors by order and make immutable copy
List<RequestInterceptor> sorted = new ArrayList<RequestInterceptor>(
interceptors != null ? interceptors : Collections.<RequestInterceptor>emptyList()
);
Collections.sort(sorted, new Comparator<RequestInterceptor>() {
@Override
public int compare(RequestInterceptor a, RequestInterceptor b) {
return Integer.compare(a.getOrder(), b.getOrder());
}
});
this.interceptors = Collections.unmodifiableList(sorted);
}
@Override
public <Req, Res> Res execute(RequestSpec<Req, Res> requestSpec) throws RocketRestException {
return executeWithRetry(requestSpec, 0);
}
/**
* Executes the request with retry support.
*/
private <Req, Res> Res executeWithRetry(RequestSpec<Req, Res> requestSpec, int retryCount)
throws RocketRestException {
final int currentRetry = retryCount;
// Create chain context for this execution
InterceptorChain chain = new InterceptorChain() {
@Override
public <R, S> S retry(RequestSpec<R, S> request) throws RocketRestException {
if (currentRetry >= maxRetries) {
throw new RocketRestException("Maximum retry count exceeded: " + maxRetries);
}
logger.debug("Retrying request (attempt {}): {} {}",
currentRetry + 1, request.getMethod(), request.getEndpoint());
return executeWithRetry(request, currentRetry + 1);
}
@Override
public int getRetryCount() {
return currentRetry;
}
@Override
public int getMaxRetries() {
return maxRetries;
}
};
// Apply beforeRequest interceptors (in order)
RequestSpec<Req, Res> currentRequest = requestSpec;
for (RequestInterceptor interceptor : interceptors) {
currentRequest = interceptor.beforeRequest(currentRequest);
}
try {
// Execute the actual request
Res response = delegate.execute(currentRequest);
// Apply afterResponse interceptors (in reverse order)
for (int i = interceptors.size() - 1; i >= 0; i--) {
response = interceptors.get(i).afterResponse(response, currentRequest);
}
return response;
} catch (RocketRestException e) {
// Apply onError interceptors (in order) - first one to return wins
for (RequestInterceptor interceptor : interceptors) {
try {
Res recovered = interceptor.onError(e, currentRequest, chain);
// If we get here, the interceptor recovered - apply afterResponse
for (int i = interceptors.size() - 1; i >= 0; i--) {
recovered = interceptors.get(i).afterResponse(recovered, currentRequest);
}
return recovered;
} catch (RocketRestException rethrown) {
// Interceptor rethrew (possibly modified) exception, continue to next
e = rethrown;
}
}
// All interceptors rethrew, propagate the last exception
throw e;
}
}
@Override
public void configureSsl(SSLContext sslContext) {
delegate.configureSsl(sslContext);
}
@Override
public void setBaseUrl(String baseUrl) {
delegate.setBaseUrl(baseUrl);
}
/**
* Gets the list of interceptors in execution order.
*
* @return Unmodifiable list of interceptors
*/
public List<RequestInterceptor> getInterceptors() {
return interceptors;
}
}