Skip to content

Commit fca583e

Browse files
committed
add UserTokenCallAdapterFactory
1 parent 10e39f7 commit fca583e

File tree

6 files changed

+227
-110
lines changed

6 files changed

+227
-110
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ public interface Client {
77
@NotNull
88
<TService> TService create(Class<TService> svcClass);
99

10-
default @NotNull <TService> TService create(Class<TService> svcClass, UserToken token) {
10+
default @NotNull <TService> TService create(Class<TService> svcClass, String userToken) {
1111
return create(svcClass);
1212
}
1313

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

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import io.jsonwebtoken.Jwts;
99
import io.jsonwebtoken.SignatureAlgorithm;
1010
import java.io.IOException;
11+
import java.lang.reflect.Proxy;
1112
import java.nio.charset.StandardCharsets;
1213
import java.security.Key;
1314
import java.time.Duration;
@@ -29,8 +30,8 @@ public class DefaultClient implements Client {
2930

3031
private static final String API_DEFAULT_URL = "https://chat.stream-io-api.com";
3132
private static volatile DefaultClient defaultInstance;
32-
@NotNull private Retrofit retrofit;
3333
@NotNull private OkHttpClient okHttpClient;
34+
@NotNull private Retrofit retrofit;
3435
@NotNull private final String apiSecret;
3536
@NotNull private final String apiKey;
3637
@NotNull private final Properties extendedProperties;
@@ -152,14 +153,30 @@ public <TService> TService create(Class<TService> svcClass) {
152153
}
153154

154155
@Override
155-
public <TService> @NotNull TService create(Class<TService> svcClass, UserToken token) {
156-
TService service = retrofit.create(svcClass);
157-
158-
return (TService) java.lang.reflect.Proxy.newProxyInstance(
159-
svcClass.getClassLoader(),
160-
new Class<?>[] { svcClass },
161-
new UserTokenCallProxy(okHttpClient, service, token)
162-
);
156+
public <TService> @NotNull TService create(Class<TService> svcClass, String userToken) {
157+
TService service = retrofit.create(svcClass);
158+
return (TService) Proxy.newProxyInstance(
159+
svcClass.getClassLoader(),
160+
new Class<?>[] { svcClass },
161+
new UserTokenCallProxy(okHttpClient, service, new UserToken(userToken))
162+
);
163+
}
164+
165+
public <TService> @NotNull TService create2(Class<TService> svcClass, String userToken) {
166+
// Create a tagged retrofit instance with a Call.Factory that tags all requests
167+
168+
okhttp3.Call.Factory taggingFactory = request -> {
169+
Request taggedRequest = request.newBuilder()
170+
.tag(UserToken.class, new UserToken(userToken))
171+
.build();
172+
return okHttpClient.newCall(taggedRequest);
173+
};
174+
175+
Retrofit taggedRetrofit = retrofit.newBuilder()
176+
.callFactory(taggingFactory)
177+
.build();
178+
179+
return taggedRetrofit.create(svcClass);
163180
}
164181

165182
@NotNull

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77
public final class UserClient implements Client {
88

99
private final Client delegate;
10-
private final UserToken userToken;
10+
private final String userToken;
1111

1212
public UserClient(Client delegate, String userToken) {
1313
this.delegate = delegate;
14-
this.userToken = new UserToken(userToken);
14+
this.userToken = userToken;
1515
}
1616

1717
@Override
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package io.getstream.chat.java.services.framework;
2+
3+
import okhttp3.Request;
4+
import org.jetbrains.annotations.NotNull;
5+
import retrofit2.Call;
6+
import retrofit2.CallAdapter;
7+
import retrofit2.Retrofit;
8+
9+
import java.io.IOException;
10+
import java.lang.annotation.Annotation;
11+
import java.lang.reflect.ParameterizedType;
12+
import java.lang.reflect.Type;
13+
14+
/**
15+
* CallAdapter.Factory that wraps Retrofit calls to inject UserToken into requests.
16+
* This is Retrofit's official API for customizing call behavior.
17+
*
18+
* Compared to reflection-based approach:
19+
* + Uses public Retrofit API (future-proof)
20+
* + Type-safe
21+
* - Still requires a Call wrapper (can't avoid it in Retrofit's design)
22+
*/
23+
class UserTokenCallAdapterFactory extends CallAdapter.Factory {
24+
private final UserToken token;
25+
private final okhttp3.Call.Factory callFactory;
26+
27+
UserTokenCallAdapterFactory(@NotNull UserToken token, @NotNull okhttp3.Call.Factory callFactory) {
28+
this.token = token;
29+
this.callFactory = callFactory;
30+
}
31+
32+
@Override
33+
public CallAdapter<?, ?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) {
34+
// Only handle Call<T> return types
35+
if (getRawType(returnType) != Call.class) {
36+
return null;
37+
}
38+
39+
if (!(returnType instanceof ParameterizedType)) {
40+
throw new IllegalStateException(
41+
"Call return type must be parameterized as Call<Foo> or Call<? extends Foo>");
42+
}
43+
44+
final Type responseType = getParameterUpperBound(0, (ParameterizedType) returnType);
45+
46+
return new CallAdapter<Object, Call<?>>() {
47+
@Override
48+
public Type responseType() {
49+
return responseType;
50+
}
51+
52+
@Override
53+
public Call<Object> adapt(Call<Object> call) {
54+
return new UserTokenCall<>(call, token, callFactory);
55+
}
56+
};
57+
}
58+
59+
/**
60+
* Wrapper that injects UserToken tag into the request before execution.
61+
*/
62+
private static class UserTokenCall<T> implements Call<T> {
63+
private final Call<T> delegate;
64+
private final UserToken token;
65+
private final okhttp3.Call.Factory callFactory;
66+
67+
UserTokenCall(Call<T> delegate, UserToken token, okhttp3.Call.Factory callFactory) {
68+
this.delegate = delegate;
69+
this.token = token;
70+
this.callFactory = callFactory;
71+
}
72+
73+
@Override
74+
public retrofit2.Response<T> execute() throws IOException {
75+
return delegate.execute();
76+
}
77+
78+
@Override
79+
public void enqueue(retrofit2.Callback<T> callback) {
80+
delegate.enqueue(callback);
81+
}
82+
83+
@Override
84+
public boolean isExecuted() {
85+
return delegate.isExecuted();
86+
}
87+
88+
@Override
89+
public void cancel() {
90+
delegate.cancel();
91+
}
92+
93+
@Override
94+
public boolean isCanceled() {
95+
return delegate.isCanceled();
96+
}
97+
98+
@Override
99+
public Call<T> clone() {
100+
return new UserTokenCall<>(delegate.clone(), token, callFactory);
101+
}
102+
103+
@Override
104+
public Request request() {
105+
// This is where the magic happens - inject the token tag
106+
return delegate.request().newBuilder()
107+
.tag(UserToken.class, token)
108+
.build();
109+
}
110+
111+
@Override
112+
public okio.Timeout timeout() {
113+
return delegate.timeout();
114+
}
115+
}
116+
}
117+
Lines changed: 81 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package io.getstream.chat.java;
22

33
import io.getstream.chat.java.models.User;
4+
import io.getstream.chat.java.services.UserService;
5+
import io.getstream.chat.java.services.framework.DefaultClient;
46
import org.junit.jupiter.api.Test;
7+
import java.lang.management.ManagementFactory;
58

69
public class CustomTest {
710

@@ -14,27 +17,85 @@ void customTest() throws Exception {
1417
}
1518

1619

17-
@Test
18-
void userReqTest() throws Exception {
19-
var userId = "han_solo";
20-
var userToken = User.createToken("han_solo", null, null);
21-
var response = User.list().filterCondition("id", userId).withUserToken(userToken).request();
22-
System.out.println("\n> " + response + "\n");
20+
@Test
21+
void userReqTest() throws Exception {
22+
var userId = "han_solo";
23+
var userToken = User.createToken("han_solo", null, null);
24+
var response = User.list().filterCondition("id", userId).withUserToken(userToken).request();
25+
System.out.println("\n> " + response + "\n");
26+
}
27+
28+
@Test
29+
void measureClientCreate() throws Exception {
30+
var userId = "han_solo";
31+
var userToken = User.createToken(userId, null, null);
32+
33+
// Test creating a UserClient directly - should use Client-Side auth
34+
var defaultClient = new DefaultClient();
35+
36+
var iterations = 10_000_000;
37+
38+
// Warm up JVM to avoid cold start effects
39+
for (int i = 0; i < 10_000; i++) {
40+
defaultClient.create(UserService.class, userToken);
41+
defaultClient.create2(UserService.class, userToken);
2342
}
2443

25-
@Test
26-
void directClientTest() throws Exception {
27-
var userId = "han_solo";
28-
var userToken = User.createToken("han_solo", null, null);
29-
30-
// Test creating a UserClient directly - should use Client-Side auth
31-
var defaultClient = io.getstream.chat.java.services.framework.Client.getInstance();
32-
var userClient = new io.getstream.chat.java.services.framework.UserClient(defaultClient, userToken);
33-
34-
var response = User.list()
35-
.filterCondition("id", userId)
36-
.withClient(userClient)
37-
.request();
38-
System.out.println("\n> Direct UserClient: " + response + "\n");
44+
// Get ThreadMXBean for accurate memory allocation tracking
45+
com.sun.management.ThreadMXBean threadBean =
46+
(com.sun.management.ThreadMXBean) ManagementFactory.getThreadMXBean();
47+
48+
// Measure first test
49+
long allocatedBefore1 = threadBean.getCurrentThreadAllocatedBytes();
50+
long startTime = System.nanoTime();
51+
for (int i = 0; i < iterations; i++) {
52+
defaultClient.create(UserService.class, userToken);
3953
}
54+
long endTime = System.nanoTime();
55+
long allocatedAfter1 = threadBean.getCurrentThreadAllocatedBytes();
56+
long elapsedTime1 = endTime - startTime;
57+
long allocated1 = allocatedAfter1 - allocatedBefore1;
58+
59+
System.out.println("=========================================================");
60+
61+
System.out.println("> First loop elapsed time: " + (elapsedTime1 / 1_000_000) + " ms");
62+
System.out.println("> First loop memory allocated: " + (allocated1 / 1024 / 1024) + " MB");
63+
System.out.println("> First loop avg time per call: " + (elapsedTime1 / (double) iterations) + " ns");
64+
System.out.println("> First loop avg memory per call: " + (allocated1 / (double) iterations) + " bytes");
65+
66+
// Measure second test
67+
long allocatedBefore2 = threadBean.getCurrentThreadAllocatedBytes();
68+
startTime = System.nanoTime();
69+
for (int i = 0; i < iterations; i++) {
70+
defaultClient.create2(UserService.class, userToken);
71+
}
72+
endTime = System.nanoTime();
73+
long allocatedAfter2 = threadBean.getCurrentThreadAllocatedBytes();
74+
long elapsedTime2 = endTime - startTime;
75+
long allocated2 = allocatedAfter2 - allocatedBefore2;
76+
77+
System.out.println("> Second loop elapsed time: " + (elapsedTime2 / 1_000_000) + " ms");
78+
System.out.println("> Second loop memory allocated: " + (allocated2 / 1024 / 1024) + " MB");
79+
System.out.println("> Second loop avg time per call: " + (elapsedTime2 / (double) iterations) + " ns");
80+
System.out.println("> Second loop avg memory per call: " + (allocated2 / (double) iterations) + " bytes");
81+
82+
// Performance comparison
83+
if (elapsedTime1 < elapsedTime2) {
84+
double timesFaster = (double) elapsedTime2 / elapsedTime1;
85+
System.out.println("> create is " + String.format("%.2fx", timesFaster) + " faster than create2");
86+
} else {
87+
double timesFaster = (double) elapsedTime1 / elapsedTime2;
88+
System.out.println("> create2 is " + String.format("%.2fx", timesFaster) + " faster than create");
89+
}
90+
91+
if (allocated1 < allocated2) {
92+
double timesLess = (double) allocated2 / allocated1;
93+
System.out.println("> create allocates " + String.format("%.2fx", timesLess) + " less memory than create2");
94+
} else {
95+
double timesLess = (double) allocated1 / allocated2;
96+
System.out.println("> create2 allocates " + String.format("%.2fx", timesLess) + " less memory than create");
97+
}
98+
99+
System.out.println("======================================================================");
100+
}
40101
}

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

Lines changed: 0 additions & 78 deletions
This file was deleted.

0 commit comments

Comments
 (0)