Skip to content

Commit 8e83576

Browse files
authored
chore: add Jspecify NullCheck in grpc transport (Fixes #334) (#342)
This PR adds Jspecify nullness annotations to the gRPC transport module: - Added @NullMarked in package-info.java for the package. - Annotated nullable parameters with @nullable in GrpcTransport.java and related classes. - Ensured all fields and method signatures match nullability expectations. - Fixed NullAway errors by initializing non-null fields and updating constructor annotations. - All tests pass successfully. --------- Co-authored-by: devcom33 <[email protected]>
1 parent 2a75e6b commit 8e83576

File tree

4 files changed

+38
-21
lines changed

4 files changed

+38
-21
lines changed

client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/GrpcTransport.java

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import io.a2a.client.transport.spi.interceptors.auth.AuthInterceptor;
1919
import io.a2a.common.A2AHeaders;
2020
import io.a2a.grpc.A2AServiceGrpc;
21+
import io.a2a.grpc.A2AServiceGrpc.A2AServiceBlockingV2Stub;
22+
import io.a2a.grpc.A2AServiceGrpc.A2AServiceStub;
2123
import io.a2a.grpc.CancelTaskRequest;
2224
import io.a2a.grpc.CreateTaskPushNotificationConfigRequest;
2325
import io.a2a.grpc.DeleteTaskPushNotificationConfigRequest;
@@ -28,7 +30,8 @@
2830
import io.a2a.grpc.SendMessageResponse;
2931
import io.a2a.grpc.StreamResponse;
3032
import io.a2a.grpc.TaskSubscriptionRequest;
31-
33+
import io.a2a.grpc.utils.ProtoUtils.FromProto;
34+
import io.a2a.grpc.utils.ProtoUtils.ToProto;
3235
import io.a2a.spec.A2AClientException;
3336
import io.a2a.spec.AgentCard;
3437
import io.a2a.spec.DeleteTaskPushNotificationConfigParams;
@@ -49,6 +52,7 @@
4952
import io.grpc.StatusRuntimeException;
5053
import io.grpc.stub.MetadataUtils;
5154
import io.grpc.stub.StreamObserver;
55+
import org.jspecify.annotations.Nullable;
5256

5357
public class GrpcTransport implements ClientTransport {
5458

@@ -60,23 +64,24 @@ public class GrpcTransport implements ClientTransport {
6064
Metadata.ASCII_STRING_MARSHALLER);
6165
private final A2AServiceBlockingV2Stub blockingStub;
6266
private final A2AServiceStub asyncStub;
63-
private final List<ClientCallInterceptor> interceptors;
67+
private final @Nullable List<ClientCallInterceptor> interceptors;
6468
private AgentCard agentCard;
6569

6670
public GrpcTransport(Channel channel, AgentCard agentCard) {
6771
this(channel, agentCard, null);
6872
}
6973

70-
public GrpcTransport(Channel channel, AgentCard agentCard, List<ClientCallInterceptor> interceptors) {
74+
public GrpcTransport(Channel channel, AgentCard agentCard, @Nullable List<ClientCallInterceptor> interceptors) {
7175
checkNotNullParam("channel", channel);
76+
checkNotNullParam("agentCard", agentCard);
7277
this.asyncStub = A2AServiceGrpc.newStub(channel);
7378
this.blockingStub = A2AServiceGrpc.newBlockingV2Stub(channel);
7479
this.agentCard = agentCard;
7580
this.interceptors = interceptors;
7681
}
7782

7883
@Override
79-
public EventKind sendMessage(MessageSendParams request, ClientCallContext context) throws A2AClientException {
84+
public EventKind sendMessage(MessageSendParams request, @Nullable ClientCallContext context) throws A2AClientException {
8085
checkNotNullParam("request", request);
8186

8287
SendMessageRequest sendMessageRequest = createGrpcSendMessageRequest(request, context);
@@ -100,7 +105,7 @@ public EventKind sendMessage(MessageSendParams request, ClientCallContext contex
100105

101106
@Override
102107
public void sendMessageStreaming(MessageSendParams request, Consumer<StreamingEventKind> eventConsumer,
103-
Consumer<Throwable> errorConsumer, ClientCallContext context) throws A2AClientException {
108+
Consumer<Throwable> errorConsumer, @Nullable ClientCallContext context) throws A2AClientException {
104109
checkNotNullParam("request", request);
105110
checkNotNullParam("eventConsumer", eventConsumer);
106111
SendMessageRequest grpcRequest = createGrpcSendMessageRequest(request, context);
@@ -117,7 +122,7 @@ public void sendMessageStreaming(MessageSendParams request, Consumer<StreamingEv
117122
}
118123

119124
@Override
120-
public Task getTask(TaskQueryParams request, ClientCallContext context) throws A2AClientException {
125+
public Task getTask(TaskQueryParams request, @Nullable ClientCallContext context) throws A2AClientException {
121126
checkNotNullParam("request", request);
122127

123128
GetTaskRequest.Builder requestBuilder = GetTaskRequest.newBuilder();
@@ -138,7 +143,7 @@ public Task getTask(TaskQueryParams request, ClientCallContext context) throws A
138143
}
139144

140145
@Override
141-
public Task cancelTask(TaskIdParams request, ClientCallContext context) throws A2AClientException {
146+
public Task cancelTask(TaskIdParams request, @Nullable ClientCallContext context) throws A2AClientException {
142147
checkNotNullParam("request", request);
143148

144149
CancelTaskRequest cancelTaskRequest = CancelTaskRequest.newBuilder()
@@ -157,7 +162,7 @@ public Task cancelTask(TaskIdParams request, ClientCallContext context) throws A
157162

158163
@Override
159164
public TaskPushNotificationConfig setTaskPushNotificationConfiguration(TaskPushNotificationConfig request,
160-
ClientCallContext context) throws A2AClientException {
165+
@Nullable ClientCallContext context) throws A2AClientException {
161166
checkNotNullParam("request", request);
162167

163168
String configId = request.pushNotificationConfig().id();
@@ -180,7 +185,7 @@ public TaskPushNotificationConfig setTaskPushNotificationConfiguration(TaskPushN
180185
@Override
181186
public TaskPushNotificationConfig getTaskPushNotificationConfiguration(
182187
GetTaskPushNotificationConfigParams request,
183-
ClientCallContext context) throws A2AClientException {
188+
@Nullable ClientCallContext context) throws A2AClientException {
184189
checkNotNullParam("request", request);
185190

186191
GetTaskPushNotificationConfigRequest grpcRequest = GetTaskPushNotificationConfigRequest.newBuilder()
@@ -200,7 +205,7 @@ public TaskPushNotificationConfig getTaskPushNotificationConfiguration(
200205
@Override
201206
public List<TaskPushNotificationConfig> listTaskPushNotificationConfigurations(
202207
ListTaskPushNotificationConfigParams request,
203-
ClientCallContext context) throws A2AClientException {
208+
@Nullable ClientCallContext context) throws A2AClientException {
204209
checkNotNullParam("request", request);
205210

206211
ListTaskPushNotificationConfigRequest grpcRequest = ListTaskPushNotificationConfigRequest.newBuilder()
@@ -221,7 +226,7 @@ public List<TaskPushNotificationConfig> listTaskPushNotificationConfigurations(
221226

222227
@Override
223228
public void deleteTaskPushNotificationConfigurations(DeleteTaskPushNotificationConfigParams request,
224-
ClientCallContext context) throws A2AClientException {
229+
@Nullable ClientCallContext context) throws A2AClientException {
225230
checkNotNullParam("request", request);
226231

227232
DeleteTaskPushNotificationConfigRequest grpcRequest = DeleteTaskPushNotificationConfigRequest.newBuilder()
@@ -240,7 +245,7 @@ public void deleteTaskPushNotificationConfigurations(DeleteTaskPushNotificationC
240245

241246
@Override
242247
public void resubscribe(TaskIdParams request, Consumer<StreamingEventKind> eventConsumer,
243-
Consumer<Throwable> errorConsumer, ClientCallContext context) throws A2AClientException {
248+
Consumer<Throwable> errorConsumer, @Nullable ClientCallContext context) throws A2AClientException {
244249
checkNotNullParam("request", request);
245250
checkNotNullParam("eventConsumer", eventConsumer);
246251

@@ -261,7 +266,7 @@ public void resubscribe(TaskIdParams request, Consumer<StreamingEventKind> event
261266
}
262267

263268
@Override
264-
public AgentCard getAgentCard(ClientCallContext context) throws A2AClientException {
269+
public AgentCard getAgentCard(@Nullable ClientCallContext context) throws A2AClientException {
265270
// TODO: Determine how to handle retrieving the authenticated extended agent card
266271
return agentCard;
267272
}
@@ -270,7 +275,7 @@ public AgentCard getAgentCard(ClientCallContext context) throws A2AClientExcepti
270275
public void close() {
271276
}
272277

273-
private SendMessageRequest createGrpcSendMessageRequest(MessageSendParams messageSendParams, ClientCallContext context) {
278+
private SendMessageRequest createGrpcSendMessageRequest(MessageSendParams messageSendParams, @Nullable ClientCallContext context) {
274279
SendMessageRequest.Builder builder = SendMessageRequest.newBuilder();
275280
builder.setRequest(ToProto.message(messageSendParams.message()));
276281
if (messageSendParams.configuration() != null) {
@@ -285,8 +290,11 @@ private SendMessageRequest createGrpcSendMessageRequest(MessageSendParams messag
285290
/**
286291
* Creates gRPC metadata from ClientCallContext headers.
287292
* Extracts headers like X-A2A-Extensions and sets them as gRPC metadata.
293+
* @param context the client call context containing headers, may be null
294+
* @param payloadAndHeaders the payload and headers wrapper, may be null
295+
* @return the gRPC metadata
288296
*/
289-
private Metadata createGrpcMetadata(ClientCallContext context, PayloadAndHeaders payloadAndHeaders) {
297+
private Metadata createGrpcMetadata(@Nullable ClientCallContext context, @Nullable PayloadAndHeaders payloadAndHeaders) {
290298
Metadata metadata = new Metadata();
291299

292300
if (context != null && context.getHeaders() != null) {
@@ -328,7 +336,7 @@ private Metadata createGrpcMetadata(ClientCallContext context, PayloadAndHeaders
328336
* @param payloadAndHeaders the payloadAndHeaders after applying any interceptors
329337
* @return blocking stub with metadata interceptor
330338
*/
331-
private A2AServiceBlockingV2Stub createBlockingStubWithMetadata(ClientCallContext context,
339+
private A2AServiceBlockingV2Stub createBlockingStubWithMetadata(@Nullable ClientCallContext context,
332340
PayloadAndHeaders payloadAndHeaders) {
333341
Metadata metadata = createGrpcMetadata(context, payloadAndHeaders);
334342
return blockingStub.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata));
@@ -341,7 +349,7 @@ private A2AServiceBlockingV2Stub createBlockingStubWithMetadata(ClientCallContex
341349
* @param payloadAndHeaders the payloadAndHeaders after applying any interceptors
342350
* @return async stub with metadata interceptor
343351
*/
344-
private A2AServiceStub createAsyncStubWithMetadata(ClientCallContext context,
352+
private A2AServiceStub createAsyncStubWithMetadata(@Nullable ClientCallContext context,
345353
PayloadAndHeaders payloadAndHeaders) {
346354
Metadata metadata = createGrpcMetadata(context, payloadAndHeaders);
347355
return asyncStub.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata));
@@ -351,7 +359,7 @@ private String getTaskPushNotificationConfigName(GetTaskPushNotificationConfigPa
351359
return getTaskPushNotificationConfigName(params.id(), params.pushNotificationConfigId());
352360
}
353361

354-
private String getTaskPushNotificationConfigName(String taskId, String pushNotificationConfigId) {
362+
private String getTaskPushNotificationConfigName(String taskId, @Nullable String pushNotificationConfigId) {
355363
StringBuilder name = new StringBuilder();
356364
name.append("tasks/");
357365
name.append(taskId);
@@ -366,7 +374,7 @@ private String getTaskPushNotificationConfigName(String taskId, String pushNotif
366374
}
367375

368376
private PayloadAndHeaders applyInterceptors(String methodName, Object payload,
369-
AgentCard agentCard, ClientCallContext clientCallContext) {
377+
AgentCard agentCard, @Nullable ClientCallContext clientCallContext) {
370378
PayloadAndHeaders payloadAndHeaders = new PayloadAndHeaders(payload,
371379
clientCallContext != null ? clientCallContext.getHeaders() : null);
372380
if (interceptors != null && ! interceptors.isEmpty()) {

client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/GrpcTransportConfigBuilder.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66

77
import java.util.function.Function;
88

9+
import org.jspecify.annotations.Nullable;
10+
911
public class GrpcTransportConfigBuilder extends ClientTransportConfigBuilder<GrpcTransportConfig, GrpcTransportConfigBuilder> {
1012

11-
private Function<String, Channel> channelFactory;
13+
private @Nullable Function<String, Channel> channelFactory;
1214

1315
public GrpcTransportConfigBuilder channelFactory(Function<String, Channel> channelFactory) {
1416
Assert.checkNotNullParam("channelFactory", channelFactory);
@@ -20,6 +22,9 @@ public GrpcTransportConfigBuilder channelFactory(Function<String, Channel> chann
2022

2123
@Override
2224
public GrpcTransportConfig build() {
25+
if (channelFactory == null) {
26+
throw new IllegalStateException("channelFactory must be set");
27+
}
2328
GrpcTransportConfig config = new GrpcTransportConfig(channelFactory);
2429
config.setInterceptors(interceptors);
2530
return config;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
@NullMarked
2+
package io.a2a.client.transport.grpc;
3+
4+
import org.jspecify.annotations.NullMarked;

client/transport/spi/src/main/java/io/a2a/client/transport/spi/interceptors/PayloadAndHeaders.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public class PayloadAndHeaders {
1010
private final @Nullable Object payload;
1111
private final Map<String, String> headers;
1212

13-
public PayloadAndHeaders(@Nullable Object payload, Map<String, String> headers) {
13+
public PayloadAndHeaders(@Nullable Object payload, @Nullable Map<String, String> headers) {
1414
this.payload = payload;
1515
this.headers = headers == null ? Collections.emptyMap() : new HashMap<>(headers);
1616
}

0 commit comments

Comments
 (0)