Skip to content

Commit 47c906f

Browse files
authored
chore: Using error prone and jspecify to expend the nullability (#332)
definition on the HTTP+REST API. # Description Defining proper contracts on Nullability for the HTTP+REST transport and client Signed-off-by: Emmanuel Hugonnet <[email protected]>
1 parent eee3bb8 commit 47c906f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+340
-275
lines changed

client/base/src/main/java/io/a2a/client/ClientBuilder.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ private ClientTransport buildClientTransport() throws A2AClientException {
9393

9494
// Get the transport provider associated to the protocol
9595
ClientTransportProvider clientTransportProvider = transportProviderRegistry.get(agentInterface.transport());
96+
if (clientTransportProvider == null) {
97+
throw new A2AClientException("No client available for " + agentInterface.transport());
98+
}
9699
Class<? extends ClientTransport> transportProtocolClass = clientTransportProvider.getTransportProtocolClass();
97100

98101
// Retrieve the configuration associated to the preferred transport

client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestErrorMapper.java

Lines changed: 15 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -48,33 +48,20 @@ public static A2AClientException mapRestError(String body, int code) {
4848
}
4949

5050
public static A2AClientException mapRestError(String className, String errorMessage, int code) {
51-
switch (className) {
52-
case "io.a2a.spec.TaskNotFoundError":
53-
return new A2AClientException(errorMessage, new TaskNotFoundError());
54-
case "io.a2a.spec.AuthenticatedExtendedCardNotConfiguredError":
55-
return new A2AClientException(errorMessage, new AuthenticatedExtendedCardNotConfiguredError());
56-
case "io.a2a.spec.ContentTypeNotSupportedError":
57-
return new A2AClientException(errorMessage, new ContentTypeNotSupportedError(null, null, errorMessage));
58-
case "io.a2a.spec.InternalError":
59-
return new A2AClientException(errorMessage, new InternalError(errorMessage));
60-
case "io.a2a.spec.InvalidAgentResponseError":
61-
return new A2AClientException(errorMessage, new InvalidAgentResponseError(null, null, errorMessage));
62-
case "io.a2a.spec.InvalidParamsError":
63-
return new A2AClientException(errorMessage, new InvalidParamsError());
64-
case "io.a2a.spec.InvalidRequestError":
65-
return new A2AClientException(errorMessage, new InvalidRequestError());
66-
case "io.a2a.spec.JSONParseError":
67-
return new A2AClientException(errorMessage, new JSONParseError());
68-
case "io.a2a.spec.MethodNotFoundError":
69-
return new A2AClientException(errorMessage, new MethodNotFoundError());
70-
case "io.a2a.spec.PushNotificationNotSupportedError":
71-
return new A2AClientException(errorMessage, new PushNotificationNotSupportedError());
72-
case "io.a2a.spec.TaskNotCancelableError":
73-
return new A2AClientException(errorMessage, new TaskNotCancelableError());
74-
case "io.a2a.spec.UnsupportedOperationError":
75-
return new A2AClientException(errorMessage, new UnsupportedOperationError());
76-
default:
77-
return new A2AClientException(errorMessage);
78-
}
51+
return switch (className) {
52+
case "io.a2a.spec.TaskNotFoundError" -> new A2AClientException(errorMessage, new TaskNotFoundError());
53+
case "io.a2a.spec.AuthenticatedExtendedCardNotConfiguredError" -> new A2AClientException(errorMessage, new AuthenticatedExtendedCardNotConfiguredError());
54+
case "io.a2a.spec.ContentTypeNotSupportedError" -> new A2AClientException(errorMessage, new ContentTypeNotSupportedError(null, null, errorMessage));
55+
case "io.a2a.spec.InternalError" -> new A2AClientException(errorMessage, new InternalError(errorMessage));
56+
case "io.a2a.spec.InvalidAgentResponseError" -> new A2AClientException(errorMessage, new InvalidAgentResponseError(null, null, errorMessage));
57+
case "io.a2a.spec.InvalidParamsError" -> new A2AClientException(errorMessage, new InvalidParamsError());
58+
case "io.a2a.spec.InvalidRequestError" -> new A2AClientException(errorMessage, new InvalidRequestError());
59+
case "io.a2a.spec.JSONParseError" -> new A2AClientException(errorMessage, new JSONParseError());
60+
case "io.a2a.spec.MethodNotFoundError" -> new A2AClientException(errorMessage, new MethodNotFoundError());
61+
case "io.a2a.spec.PushNotificationNotSupportedError" -> new A2AClientException(errorMessage, new PushNotificationNotSupportedError());
62+
case "io.a2a.spec.TaskNotCancelableError" -> new A2AClientException(errorMessage, new TaskNotCancelableError());
63+
case "io.a2a.spec.UnsupportedOperationError" -> new A2AClientException(errorMessage, new UnsupportedOperationError());
64+
default -> new A2AClientException(errorMessage);
65+
};
7966
}
8067
}

client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransport.java

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -38,40 +38,38 @@
3838
import io.a2a.spec.SetTaskPushNotificationConfigRequest;
3939
import io.a2a.util.Utils;
4040
import java.io.IOException;
41+
import java.util.Collections;
4142
import java.util.List;
4243
import java.util.logging.Logger;
4344
import java.util.Map;
4445
import java.util.concurrent.CompletableFuture;
4546
import java.util.concurrent.atomic.AtomicReference;
4647
import java.util.function.Consumer;
48+
import org.jspecify.annotations.Nullable;
4749

4850
public class RestTransport implements ClientTransport {
4951

5052
private static final Logger log = Logger.getLogger(RestTransport.class.getName());
5153
private final A2AHttpClient httpClient;
5254
private final String agentUrl;
53-
private final List<ClientCallInterceptor> interceptors;
55+
private @Nullable final List<ClientCallInterceptor> interceptors;
5456
private AgentCard agentCard;
5557
private boolean needsExtendedCard = false;
5658

57-
public RestTransport(String agentUrl) {
58-
this(null, null, agentUrl, null);
59-
}
60-
6159
public RestTransport(AgentCard agentCard) {
6260
this(null, agentCard, agentCard.url(), null);
6361
}
6462

65-
public RestTransport(A2AHttpClient httpClient, AgentCard agentCard,
66-
String agentUrl, List<ClientCallInterceptor> interceptors) {
63+
public RestTransport(@Nullable A2AHttpClient httpClient, AgentCard agentCard,
64+
String agentUrl, @Nullable List<ClientCallInterceptor> interceptors) {
6765
this.httpClient = httpClient == null ? new JdkA2AHttpClient() : httpClient;
6866
this.agentCard = agentCard;
6967
this.agentUrl = agentUrl.endsWith("/") ? agentUrl.substring(0, agentUrl.length() - 1) : agentUrl;
7068
this.interceptors = interceptors;
7169
}
7270

7371
@Override
74-
public EventKind sendMessage(MessageSendParams messageSendParams, ClientCallContext context) throws A2AClientException {
72+
public EventKind sendMessage(MessageSendParams messageSendParams, @Nullable ClientCallContext context) throws A2AClientException {
7573
checkNotNullParam("messageSendParams", messageSendParams);
7674
io.a2a.grpc.SendMessageRequest.Builder builder = io.a2a.grpc.SendMessageRequest.newBuilder(ProtoUtils.ToProto.sendMessageRequest(messageSendParams));
7775
PayloadAndHeaders payloadAndHeaders = applyInterceptors(io.a2a.spec.SendMessageRequest.METHOD, builder, agentCard, context);
@@ -94,7 +92,7 @@ public EventKind sendMessage(MessageSendParams messageSendParams, ClientCallCont
9492
}
9593

9694
@Override
97-
public void sendMessageStreaming(MessageSendParams messageSendParams, Consumer<StreamingEventKind> eventConsumer, Consumer<Throwable> errorConsumer, ClientCallContext context) throws A2AClientException {
95+
public void sendMessageStreaming(MessageSendParams messageSendParams, Consumer<StreamingEventKind> eventConsumer, Consumer<Throwable> errorConsumer, @Nullable ClientCallContext context) throws A2AClientException {
9896
checkNotNullParam("request", messageSendParams);
9997
checkNotNullParam("eventConsumer", eventConsumer);
10098
checkNotNullParam("messageSendParams", messageSendParams);
@@ -119,7 +117,7 @@ public void sendMessageStreaming(MessageSendParams messageSendParams, Consumer<S
119117
}
120118

121119
@Override
122-
public Task getTask(TaskQueryParams taskQueryParams, ClientCallContext context) throws A2AClientException {
120+
public Task getTask(TaskQueryParams taskQueryParams, @Nullable ClientCallContext context) throws A2AClientException {
123121
checkNotNullParam("taskQueryParams", taskQueryParams);
124122
GetTaskRequest.Builder builder = GetTaskRequest.newBuilder();
125123
builder.setName("tasks/" + taskQueryParams.id());
@@ -154,7 +152,7 @@ public Task getTask(TaskQueryParams taskQueryParams, ClientCallContext context)
154152
}
155153

156154
@Override
157-
public Task cancelTask(TaskIdParams taskIdParams, ClientCallContext context) throws A2AClientException {
155+
public Task cancelTask(TaskIdParams taskIdParams, @Nullable ClientCallContext context) throws A2AClientException {
158156
checkNotNullParam("taskIdParams", taskIdParams);
159157
CancelTaskRequest.Builder builder = CancelTaskRequest.newBuilder();
160158
builder.setName("tasks/" + taskIdParams.id());
@@ -173,7 +171,7 @@ public Task cancelTask(TaskIdParams taskIdParams, ClientCallContext context) thr
173171
}
174172

175173
@Override
176-
public TaskPushNotificationConfig setTaskPushNotificationConfiguration(TaskPushNotificationConfig request, ClientCallContext context) throws A2AClientException {
174+
public TaskPushNotificationConfig setTaskPushNotificationConfiguration(TaskPushNotificationConfig request, @Nullable ClientCallContext context) throws A2AClientException {
177175
checkNotNullParam("request", request);
178176
CreateTaskPushNotificationConfigRequest.Builder builder = CreateTaskPushNotificationConfigRequest.newBuilder();
179177
builder.setConfig(ProtoUtils.ToProto.taskPushNotificationConfig(request))
@@ -195,7 +193,7 @@ public TaskPushNotificationConfig setTaskPushNotificationConfiguration(TaskPushN
195193
}
196194

197195
@Override
198-
public TaskPushNotificationConfig getTaskPushNotificationConfiguration(GetTaskPushNotificationConfigParams request, ClientCallContext context) throws A2AClientException {
196+
public TaskPushNotificationConfig getTaskPushNotificationConfiguration(GetTaskPushNotificationConfigParams request, @Nullable ClientCallContext context) throws A2AClientException {
199197
checkNotNullParam("request", request);
200198
GetTaskPushNotificationConfigRequest.Builder builder = GetTaskPushNotificationConfigRequest.newBuilder();
201199
builder.setName(String.format("/tasks/%1s/pushNotificationConfigs/%2s", request.id(), request.pushNotificationConfigId()));
@@ -225,7 +223,7 @@ public TaskPushNotificationConfig getTaskPushNotificationConfiguration(GetTaskPu
225223
}
226224

227225
@Override
228-
public List<TaskPushNotificationConfig> listTaskPushNotificationConfigurations(ListTaskPushNotificationConfigParams request, ClientCallContext context) throws A2AClientException {
226+
public List<TaskPushNotificationConfig> listTaskPushNotificationConfigurations(ListTaskPushNotificationConfigParams request, @Nullable ClientCallContext context) throws A2AClientException {
229227
checkNotNullParam("request", request);
230228
ListTaskPushNotificationConfigRequest.Builder builder = ListTaskPushNotificationConfigRequest.newBuilder();
231229
builder.setParent(String.format("/tasks/%1s/pushNotificationConfigs", request.id()));
@@ -255,7 +253,7 @@ public List<TaskPushNotificationConfig> listTaskPushNotificationConfigurations(L
255253
}
256254

257255
@Override
258-
public void deleteTaskPushNotificationConfigurations(DeleteTaskPushNotificationConfigParams request, ClientCallContext context) throws A2AClientException {
256+
public void deleteTaskPushNotificationConfigurations(DeleteTaskPushNotificationConfigParams request, @Nullable ClientCallContext context) throws A2AClientException {
259257
checkNotNullParam("request", request);
260258
io.a2a.grpc.DeleteTaskPushNotificationConfigRequestOrBuilder builder = io.a2a.grpc.DeleteTaskPushNotificationConfigRequest.newBuilder();
261259
PayloadAndHeaders payloadAndHeaders = applyInterceptors(io.a2a.spec.DeleteTaskPushNotificationConfigRequest.METHOD, builder,
@@ -281,7 +279,7 @@ public void deleteTaskPushNotificationConfigurations(DeleteTaskPushNotificationC
281279

282280
@Override
283281
public void resubscribe(TaskIdParams request, Consumer<StreamingEventKind> eventConsumer,
284-
Consumer<Throwable> errorConsumer, ClientCallContext context) throws A2AClientException {
282+
Consumer<Throwable> errorConsumer, @Nullable ClientCallContext context) throws A2AClientException {
285283
checkNotNullParam("request", request);
286284
io.a2a.grpc.TaskSubscriptionRequest.Builder builder = io.a2a.grpc.TaskSubscriptionRequest.newBuilder();
287285
builder.setName("tasks/" + request.id());
@@ -306,7 +304,7 @@ public void resubscribe(TaskIdParams request, Consumer<StreamingEventKind> event
306304
}
307305

308306
@Override
309-
public AgentCard getAgentCard(ClientCallContext context) throws A2AClientException {
307+
public AgentCard getAgentCard(@Nullable ClientCallContext context) throws A2AClientException {
310308
A2ACardResolver resolver;
311309
try {
312310
if (agentCard == null) {
@@ -346,8 +344,8 @@ public void close() {
346344
// no-op
347345
}
348346

349-
private PayloadAndHeaders applyInterceptors(String methodName, MessageOrBuilder payload,
350-
AgentCard agentCard, ClientCallContext clientCallContext) {
347+
private PayloadAndHeaders applyInterceptors(String methodName, @Nullable MessageOrBuilder payload,
348+
AgentCard agentCard, @Nullable ClientCallContext clientCallContext) {
351349
PayloadAndHeaders payloadAndHeaders = new PayloadAndHeaders(payload, getHttpHeaders(clientCallContext));
352350
if (interceptors != null && !interceptors.isEmpty()) {
353351
for (ClientCallInterceptor interceptor : interceptors) {
@@ -383,7 +381,7 @@ private A2AHttpClient.PostBuilder createPostBuilder(String url, PayloadAndHeader
383381
return postBuilder;
384382
}
385383

386-
private Map<String, String> getHttpHeaders(ClientCallContext context) {
387-
return context != null ? context.getHeaders() : null;
384+
private Map<String, String> getHttpHeaders(@Nullable ClientCallContext context) {
385+
return context != null ? context.getHeaders() : Collections.emptyMap();
388386
}
389387
}

client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransportConfig.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
import io.a2a.client.http.A2AHttpClient;
44
import io.a2a.client.transport.spi.ClientTransportConfig;
5+
import org.jspecify.annotations.Nullable;
56

67
public class RestTransportConfig extends ClientTransportConfig<RestTransport> {
78

8-
private final A2AHttpClient httpClient;
9+
private final @Nullable A2AHttpClient httpClient;
910

1011
public RestTransportConfig() {
1112
this.httpClient = null;
@@ -15,7 +16,7 @@ public RestTransportConfig(A2AHttpClient httpClient) {
1516
this.httpClient = httpClient;
1617
}
1718

18-
public A2AHttpClient getHttpClient() {
19+
public @Nullable A2AHttpClient getHttpClient() {
1920
return httpClient;
2021
}
2122
}

client/transport/rest/src/main/java/io/a2a/client/transport/rest/RestTransportConfigBuilder.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
import io.a2a.client.http.A2AHttpClient;
44
import io.a2a.client.http.JdkA2AHttpClient;
55
import io.a2a.client.transport.spi.ClientTransportConfigBuilder;
6+
import org.jspecify.annotations.Nullable;
67

78
public class RestTransportConfigBuilder extends ClientTransportConfigBuilder<RestTransportConfig, RestTransportConfigBuilder> {
89

9-
private A2AHttpClient httpClient;
10+
private @Nullable A2AHttpClient httpClient;
1011

1112
public RestTransportConfigBuilder httpClient(A2AHttpClient httpClient) {
1213
this.httpClient = httpClient;
13-
1414
return this;
1515
}
1616

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@NullMarked
2+
package io.a2a.client.transport.rest;
3+
4+
import org.jspecify.annotations.NullMarked;
5+

client/transport/rest/src/main/java/io/a2a/client/transport/rest/sse/RestSSEEventListener.java

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import io.a2a.grpc.StreamResponse;
1616
import io.a2a.grpc.utils.ProtoUtils;
1717
import io.a2a.spec.StreamingEventKind;
18+
import org.jspecify.annotations.Nullable;
1819

1920
public class RestSSEEventListener {
2021

@@ -28,43 +29,42 @@ public RestSSEEventListener(Consumer<StreamingEventKind> eventHandler,
2829
this.errorHandler = errorHandler;
2930
}
3031

31-
public void onMessage(String message, Future<Void> completableFuture) {
32+
public void onMessage(String message, @Nullable Future<Void> completableFuture) {
3233
try {
33-
System.out.println("Streaming message received: " + message);
34+
log.fine("Streaming message received: " + message);
3435
io.a2a.grpc.StreamResponse.Builder builder = io.a2a.grpc.StreamResponse.newBuilder();
3536
JsonFormat.parser().merge(message, builder);
36-
handleMessage(builder.build(), completableFuture);
37+
handleMessage(builder.build());
3738
} catch (InvalidProtocolBufferException e) {
3839
errorHandler.accept(RestErrorMapper.mapRestError(message, 500));
3940
}
4041
}
4142

42-
public void onError(Throwable throwable, Future<Void> future) {
43+
public void onError(Throwable throwable, @Nullable Future<Void> future) {
4344
if (errorHandler != null) {
4445
errorHandler.accept(throwable);
4546
}
46-
future.cancel(true); // close SSE channel
47+
if (future != null) {
48+
future.cancel(true); // close SSE channel
49+
}
4750
}
4851

49-
private void handleMessage(StreamResponse response, Future<Void> future) {
52+
private void handleMessage(StreamResponse response) {
5053
StreamingEventKind event;
5154
switch (response.getPayloadCase()) {
52-
case MSG:
55+
case MSG ->
5356
event = ProtoUtils.FromProto.message(response.getMsg());
54-
break;
55-
case TASK:
57+
case TASK ->
5658
event = ProtoUtils.FromProto.task(response.getTask());
57-
break;
58-
case STATUS_UPDATE:
59+
case STATUS_UPDATE ->
5960
event = ProtoUtils.FromProto.taskStatusUpdateEvent(response.getStatusUpdate());
60-
break;
61-
case ARTIFACT_UPDATE:
61+
case ARTIFACT_UPDATE ->
6262
event = ProtoUtils.FromProto.taskArtifactUpdateEvent(response.getArtifactUpdate());
63-
break;
64-
default:
63+
default -> {
6564
log.warning("Invalid stream response " + response.getPayloadCase());
6665
errorHandler.accept(new IllegalStateException("Invalid stream response from server: " + response.getPayloadCase()));
6766
return;
67+
}
6868
}
6969
eventHandler.accept(event);
7070
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@NullMarked
2+
package io.a2a.client.transport.rest.sse;
3+
4+
import org.jspecify.annotations.NullMarked;
5+

0 commit comments

Comments
 (0)