Skip to content

Commit cafc8cc

Browse files
committed
improve UserCall
1 parent dda6149 commit cafc8cc

File tree

3 files changed

+179
-18
lines changed

3 files changed

+179
-18
lines changed

src/main/java/io/getstream/chat/java/services/framework/UserCall.java

Lines changed: 165 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,204 @@
11
package io.getstream.chat.java.services.framework;
22

33
import okhttp3.Request;
4+
import okhttp3.ResponseBody;
45
import org.jetbrains.annotations.NotNull;
6+
import retrofit2.Retrofit;
57

68
import java.io.IOException;
9+
import java.lang.annotation.Annotation;
10+
import java.lang.reflect.Type;
711

812
/**
913
* Wrapper for Retrofit {@code Call} objects that injects user authentication tokens.
1014
* <p>
11-
* This class delegates all {@code Call} operations to an underlying call while ensuring
12-
* that the {@link UserToken} is attached to the request as a typed tag. The token can
13-
* then be retrieved by OkHttp interceptors for adding authorization headers.
15+
* This class creates new OkHttp calls using the tagged request to ensure the {@link UserToken}
16+
* is properly attached and available to interceptors for adding authorization headers.
1417
* </p>
1518
*
1619
* @param <T> the response body type
1720
* @see UserToken
18-
* @see UserTokenCallRewriter
1921
*/
2022
class UserCall<T> implements retrofit2.Call<T> {
2123
private final retrofit2.Call<T> delegate;
2224
private final UserToken token;
25+
private final Retrofit retrofit;
26+
private final Type responseType;
27+
private volatile boolean executed;
28+
private volatile okhttp3.Call rawCall;
2329

2430
/**
2531
* Constructs a new UserCall that wraps the provided call with token injection.
2632
*
27-
* @param delegate the underlying Retrofit call
33+
* @param delegate the underlying Retrofit call (used for request template)
2834
* @param token the user token to inject
35+
* @param retrofit the Retrofit instance for creating calls and parsing responses
36+
* @param responseType the actual response type for proper deserialization
2937
*/
30-
UserCall(retrofit2.Call<T> delegate, UserToken token) {
38+
UserCall(retrofit2.Call<T> delegate, UserToken token, Retrofit retrofit, Type responseType) {
3139
this.delegate = delegate;
3240
this.token = token;
41+
this.retrofit = retrofit;
42+
this.responseType = responseType;
3343
}
3444

3545
/**
36-
* Executes the HTTP request synchronously.
46+
* Creates an OkHttp call with the tagged request.
47+
*/
48+
private okhttp3.Call createRawCall() {
49+
return retrofit.callFactory().newCall(request());
50+
}
51+
52+
/**
53+
* Executes the HTTP request synchronously using a new call with the tagged request.
3754
*
3855
* @return the response
3956
* @throws IOException if the request fails
4057
*/
4158
@Override
4259
public @NotNull retrofit2.Response<T> execute() throws IOException {
43-
return delegate.execute();
60+
okhttp3.Call call;
61+
synchronized (this) {
62+
if (executed) throw new IllegalStateException("Already executed.");
63+
executed = true;
64+
rawCall = createRawCall();
65+
call = rawCall;
66+
}
67+
68+
okhttp3.Response rawResponse = call.execute();
69+
return parseResponse(rawResponse);
4470
}
4571

4672
/**
47-
* Asynchronously sends the request and notifies the callback of its response.
73+
* Asynchronously sends the request using a new call with the tagged request.
4874
*
4975
* @param callback the callback to notify when the response arrives
5076
*/
5177
@Override
5278
public void enqueue(@NotNull retrofit2.Callback<T> callback) {
53-
delegate.enqueue(callback);
79+
okhttp3.Call call;
80+
synchronized (this) {
81+
if (executed) throw new IllegalStateException("Already executed.");
82+
executed = true;
83+
rawCall = createRawCall();
84+
call = rawCall;
85+
}
86+
87+
call.enqueue(new okhttp3.Callback() {
88+
@Override
89+
public void onResponse(@NotNull okhttp3.Call call, @NotNull okhttp3.Response rawResponse) {
90+
retrofit2.Response<T> response;
91+
try {
92+
response = parseResponse(rawResponse);
93+
} catch (Throwable t) {
94+
callFailure(t);
95+
return;
96+
}
97+
callSuccess(response);
98+
}
99+
100+
@Override
101+
public void onFailure(@NotNull okhttp3.Call call, @NotNull IOException e) {
102+
callFailure(e);
103+
}
104+
105+
private void callSuccess(retrofit2.Response<T> response) {
106+
try {
107+
callback.onResponse(UserCall.this, response);
108+
} catch (Throwable t) {
109+
t.printStackTrace();
110+
}
111+
}
112+
113+
private void callFailure(Throwable t) {
114+
try {
115+
callback.onFailure(UserCall.this, t);
116+
} catch (Throwable t2) {
117+
t2.printStackTrace();
118+
}
119+
}
120+
});
121+
}
122+
123+
/**
124+
* Parses the raw OkHttp response into a Retrofit response using Retrofit's converters.
125+
* Based on Retrofit's OkHttpCall.parseResponse() implementation.
126+
*/
127+
@SuppressWarnings("unchecked")
128+
private retrofit2.Response<T> parseResponse(okhttp3.Response rawResponse) throws IOException {
129+
ResponseBody rawBody = rawResponse.body();
130+
131+
// Remove the body's source (the only stateful object) so we can pass the response along
132+
rawResponse = rawResponse.newBuilder()
133+
.body(new NoContentResponseBody(rawBody.contentType(), rawBody.contentLength()))
134+
.build();
135+
136+
int code = rawResponse.code();
137+
138+
if (code < 200 || code >= 300) {
139+
try {
140+
// Buffer the entire body to avoid future I/O
141+
ResponseBody bufferedBody = bufferResponseBody(rawBody);
142+
return retrofit2.Response.error(bufferedBody, rawResponse);
143+
} finally {
144+
rawBody.close();
145+
}
146+
}
147+
148+
if (code == 204 || code == 205) {
149+
rawBody.close();
150+
return retrofit2.Response.success(null, rawResponse);
151+
}
152+
153+
// Success response - parse body using Retrofit's converter
154+
try {
155+
retrofit2.Converter<ResponseBody, T> converter =
156+
(retrofit2.Converter<ResponseBody, T>) retrofit.responseBodyConverter(
157+
responseType, new Annotation[0]);
158+
159+
T body = converter.convert(rawBody);
160+
return retrofit2.Response.success(body, rawResponse);
161+
} catch (RuntimeException e) {
162+
rawBody.close();
163+
throw e;
164+
}
165+
}
166+
167+
/**
168+
* Buffers the response body to avoid future I/O operations.
169+
*/
170+
private static ResponseBody bufferResponseBody(ResponseBody body) throws IOException {
171+
okio.Buffer buffer = new okio.Buffer();
172+
body.source().readAll(buffer);
173+
return ResponseBody.create(buffer.readByteArray(), body.contentType());
174+
}
175+
176+
/**
177+
* A response body that returns empty content, used to prevent reading stateful sources.
178+
*/
179+
private static final class NoContentResponseBody extends ResponseBody {
180+
private final okhttp3.MediaType contentType;
181+
private final long contentLength;
182+
183+
NoContentResponseBody(okhttp3.MediaType contentType, long contentLength) {
184+
this.contentType = contentType;
185+
this.contentLength = contentLength;
186+
}
187+
188+
@Override
189+
public okhttp3.MediaType contentType() {
190+
return contentType;
191+
}
192+
193+
@Override
194+
public long contentLength() {
195+
return contentLength;
196+
}
197+
198+
@Override
199+
public okio.BufferedSource source() {
200+
throw new IllegalStateException("Cannot read raw response body of a converted body.");
201+
}
54202
}
55203

56204
/**
@@ -60,15 +208,17 @@ public void enqueue(@NotNull retrofit2.Callback<T> callback) {
60208
*/
61209
@Override
62210
public boolean isExecuted() {
63-
return delegate.isExecuted();
211+
return executed;
64212
}
65213

66214
/**
67215
* Cancels the request, if possible.
68216
*/
69217
@Override
70218
public void cancel() {
71-
delegate.cancel();
219+
if (rawCall != null) {
220+
rawCall.cancel();
221+
}
72222
}
73223

74224
/**
@@ -78,7 +228,7 @@ public void cancel() {
78228
*/
79229
@Override
80230
public boolean isCanceled() {
81-
return delegate.isCanceled();
231+
return rawCall != null && rawCall.isCanceled();
82232
}
83233

84234
/**
@@ -91,7 +241,7 @@ public boolean isCanceled() {
91241
*/
92242
@Override
93243
public @NotNull retrofit2.Call<T> clone() {
94-
return new UserCall<>(delegate.clone(), token);
244+
return new UserCall<>(delegate.clone(), token, retrofit, responseType);
95245
}
96246

97247
/**
@@ -118,6 +268,6 @@ public boolean isCanceled() {
118268
*/
119269
@Override
120270
public @NotNull okio.Timeout timeout() {
121-
return delegate.timeout();
271+
return rawCall != null ? rawCall.timeout() : okio.Timeout.NONE;
122272
}
123273
}

src/main/java/io/getstream/chat/java/services/framework/UserServiceFactoryCall.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,19 @@ public final <TService> TService create(Class<TService> svcClass, UserToken user
5555
Object result = method.invoke(delegate, args);
5656

5757
// If the result is a retrofit2.Call, wrap it with UserCall
58-
if (result instanceof retrofit2.Call) {
59-
return new UserCall<>((retrofit2.Call<?>) result, userToken);
58+
if (result instanceof retrofit2.Call<?>) {
59+
// Extract the response type from the method's return type
60+
java.lang.reflect.Type returnType = method.getGenericReturnType();
61+
java.lang.reflect.Type responseType = Object.class;
62+
63+
if (returnType instanceof java.lang.reflect.ParameterizedType) {
64+
java.lang.reflect.ParameterizedType paramType = (java.lang.reflect.ParameterizedType) returnType;
65+
if (paramType.getActualTypeArguments().length > 0) {
66+
responseType = paramType.getActualTypeArguments()[0];
67+
}
68+
}
69+
70+
return new UserCall<>((retrofit2.Call<?>) result, userToken, retrofit, responseType);
6071
}
6172

6273
return result;

src/test/java/io/getstream/chat/java/CustomTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public class CustomTest {
1818
void customTest() throws Exception {
1919
var userId = "admin";
2020
var userToken = User.createToken(userId, null, null);
21-
var response = User.list().userId(userId).filterCondition("id", userId).request();
21+
var response = User.list().filterCondition("id", userId).withUserToken(userToken).request();
2222
System.out.println(response);
2323
}
2424

0 commit comments

Comments
 (0)