Skip to content

Commit 46bbd2b

Browse files
committed
chore: Migrate spec module to JSpecify annotations for null-safety
- Add @NullMarked to io.a2a.spec package - Apply @nullable annotations to optional fields and parameters - Move builder validation to build() methods using Assert.checkNotNullParam() - Fix null-safety issues in RequestContext ID generation - Add null checks for push notification configuration - Simplify REST route handling for push notification configs - Update tests to provide required MessageSendConfiguration Signed-off-by: Emmanuel Hugonnet <ehugonne@redhat.com>
1 parent 02ba508 commit 46bbd2b

File tree

52 files changed

+441
-378
lines changed

Some content is hidden

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

52 files changed

+441
-378
lines changed

common/src/main/java/io/a2a/util/Assert.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.a2a.util;
22

3+
import org.jspecify.annotations.Nullable;
4+
35
public final class Assert {
46

57
/**
@@ -11,18 +13,21 @@ public final class Assert {
1113
* @return the value that was passed in
1214
* @throws IllegalArgumentException if the value is {@code null}
1315
*/
14-
@NotNull
15-
public static <T> T checkNotNullParam(String name, T value) throws IllegalArgumentException {
16+
public static <T> @NotNull T checkNotNullParam(String name, @Nullable T value) throws IllegalArgumentException {
1617
checkNotNullParamChecked("name", name);
17-
checkNotNullParamChecked(name, value);
18+
if (value == null) {
19+
throw new IllegalArgumentException("Parameter '" + name + "' may not be null");
20+
}
1821
return value;
1922
}
2023

21-
private static <T> void checkNotNullParamChecked(final String name, final T value) {
22-
if (value == null) throw new IllegalArgumentException("Parameter '" + name + "' may not be null");
24+
private static <T> void checkNotNullParamChecked(final String name, final @Nullable T value) {
25+
if (value == null) {
26+
throw new IllegalArgumentException("Parameter '" + name + "' may not be null");
27+
}
2328
}
2429

25-
public static void isNullOrStringOrInteger(Object value) {
30+
public static void isNullOrStringOrInteger(@Nullable Object value) {
2631
if (! (value == null || value instanceof String || value instanceof Integer)) {
2732
throw new IllegalArgumentException("Id must be null, a String, or an Integer");
2833
}

jsonrpc-common/src/main/java/io/a2a/jsonrpc/common/json/JsonUtil.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,13 @@
2222
import java.time.OffsetDateTime;
2323
import java.time.format.DateTimeFormatter;
2424
import java.time.format.DateTimeParseException;
25-
import java.util.Map;
2625
import java.util.Set;
2726

2827
import com.google.gson.Gson;
2928
import com.google.gson.GsonBuilder;
3029
import com.google.gson.JsonSyntaxException;
3130
import com.google.gson.ToNumberPolicy;
3231
import com.google.gson.TypeAdapter;
33-
import com.google.gson.reflect.TypeToken;
3432
import com.google.gson.stream.JsonReader;
3533
import com.google.gson.stream.JsonToken;
3634
import com.google.gson.stream.JsonWriter;
@@ -435,7 +433,7 @@ private A2AError createErrorInstance(@Nullable Integer code, @Nullable String me
435433
case INVALID_AGENT_RESPONSE_ERROR_CODE ->
436434
new InvalidAgentResponseError(code, message, data);
437435
default ->
438-
new A2AError(code, message, data);
436+
new A2AError(code, message == null ? "" : message, data);
439437
};
440438
}
441439
}

reference/rest/src/main/java/io/a2a/server/rest/quarkus/A2AServerRoutes.java

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,9 @@ public void getTaskPushNotificationConfiguration(RoutingContext rc) {
289289
try {
290290
if (taskId == null || taskId.isEmpty()) {
291291
response = jsonRestHandler.createErrorResponse(new InvalidParamsError("bad task id"));
292-
} else {
292+
} else if (configId == null || configId.isEmpty()) {
293+
response = jsonRestHandler.createErrorResponse(new InvalidParamsError("bad configuration id"));
294+
}else {
293295
response = jsonRestHandler.getTaskPushNotificationConfiguration(taskId, configId, extractTenant(rc), context);
294296
}
295297
} catch (Throwable t) {
@@ -299,26 +301,7 @@ public void getTaskPushNotificationConfiguration(RoutingContext rc) {
299301
}
300302
}
301303

302-
@Route(regex = "^\\/(?<tenant>[^\\/]*\\/?)tasks\\/(?<taskId>[^/]+)\\/pushNotificationConfigs\\/$", order = 1, methods = {Route.HttpMethod.GET}, type = Route.HandlerType.BLOCKING)
303-
public void getTaskPushNotificationConfigurationWithoutId(RoutingContext rc) {
304-
String taskId = rc.pathParam("taskId");
305-
ServerCallContext context = createCallContext(rc, GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD);
306-
HTTPRestResponse response = null;
307-
try {
308-
if (taskId == null || taskId.isEmpty()) {
309-
response = jsonRestHandler.createErrorResponse(new InvalidParamsError("bad task id"));
310-
} else {
311-
// Call get with null configId - trailing slash distinguishes this from list
312-
response = jsonRestHandler.getTaskPushNotificationConfiguration(taskId, null, extractTenant(rc), context);
313-
}
314-
} catch (Throwable t) {
315-
response = jsonRestHandler.createErrorResponse(new InternalError(t.getMessage()));
316-
} finally {
317-
sendResponse(rc, response);
318-
}
319-
}
320-
321-
@Route(regex = "^\\/(?<tenant>[^\\/]*\\/?)tasks\\/(?<taskId>[^/]+)\\/pushNotificationConfigs", order = 3, methods = {Route.HttpMethod.GET}, type = Route.HandlerType.BLOCKING)
304+
@Route(regex = "^\\/(?<tenant>[^\\/]*\\/?)tasks\\/(?<taskId>[^/]+)\\/pushNotificationConfigs\\/?$", order = 3, methods = {Route.HttpMethod.GET}, type = Route.HandlerType.BLOCKING)
322305
public void listTaskPushNotificationConfigurations(RoutingContext rc) {
323306
String taskId = rc.pathParam("taskId");
324307
ServerCallContext context = createCallContext(rc, LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD);

server-common/src/main/java/io/a2a/server/agentexecution/RequestContext.java

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -99,14 +99,12 @@ public RequestContext(
9999
if (params != null) {
100100
if (taskId != null && !taskId.equals(params.message().taskId())) {
101101
throw new InvalidParamsError("bad task id");
102-
} else {
103-
checkOrGenerateTaskId();
104102
}
103+
this.taskId = checkOrGenerateTaskId();
105104
if (contextId != null && !contextId.equals(params.message().contextId())) {
106105
throw new InvalidParamsError("bad context id");
107-
} else {
108-
checkOrGenerateContextId();
109106
}
107+
this.contextId = checkOrGenerateContextId();
110108
}
111109
}
112110

@@ -246,9 +244,9 @@ public void attachRelatedTask(Task task) {
246244
relatedTasks.add(task);
247245
}
248246

249-
private void checkOrGenerateTaskId() {
247+
private @Nullable String checkOrGenerateTaskId() {
250248
if (params == null) {
251-
return;
249+
return taskId;
252250
}
253251
if (taskId == null && params.message().taskId() == null) {
254252
// Message is immutable, create new one with generated taskId
@@ -257,15 +255,17 @@ private void checkOrGenerateTaskId() {
257255
.taskId(generatedTaskId)
258256
.build();
259257
params = new MessageSendParams(updatedMessage, params.configuration(), params.metadata());
260-
this.taskId = generatedTaskId;
261-
} else if (params.message().taskId() != null) {
262-
this.taskId = params.message().taskId();
258+
return generatedTaskId;
259+
}
260+
if (params.message().taskId() != null) {
261+
return params.message().taskId();
263262
}
263+
return taskId;
264264
}
265265

266-
private void checkOrGenerateContextId() {
266+
private @Nullable String checkOrGenerateContextId() {
267267
if (params == null) {
268-
return;
268+
return contextId;
269269
}
270270
if (contextId == null && params.message().contextId() == null) {
271271
// Message is immutable, create new one with generated contextId
@@ -274,10 +274,12 @@ private void checkOrGenerateContextId() {
274274
.contextId(generatedContextId)
275275
.build();
276276
params = new MessageSendParams(updatedMessage, params.configuration(), params.metadata());
277-
this.contextId = generatedContextId;
278-
} else if (params.message().contextId() != null) {
279-
this.contextId = params.message().contextId();
277+
return generatedContextId;
278+
}
279+
if (params.message().contextId() != null) {
280+
return params.message().contextId();
280281
}
282+
return contextId;
281283
}
282284

283285
private String getMessageText(Message message, String delimiter) {

server-common/src/main/java/io/a2a/server/requesthandlers/DefaultRequestHandler.java

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,8 @@ public EventKind onMessageSend(MessageSendParams params, ServerCallContext conte
472472

473473
// Store push notification config for newly created tasks (mirrors streaming logic)
474474
// Only for NEW tasks - existing tasks are handled by initMessageSend()
475-
if (mss.task() == null && kind instanceof Task createdTask && shouldAddPushInfo(params)) {
475+
if (mss.task() == null && kind instanceof Task createdTask && pushConfigStore != null
476+
&& params.configuration() != null && params.configuration().pushNotificationConfig() != null) {
476477
LOGGER.debug("Storing push notification config for new task {} (original taskId from params: {})",
477478
createdTask.id(), params.message().taskId());
478479
pushConfigStore.setInfo(createdTask.id(), params.configuration().pushNotificationConfig());
@@ -550,7 +551,7 @@ public EventKind onMessageSend(MessageSendParams params, ServerCallContext conte
550551
kind = updatedTask;
551552
LOGGER.debug("DefaultRequestHandler: Step 5 - Fetched current task for {} with state {} and {} artifacts",
552553
taskId.get(), updatedTask.status().state(),
553-
updatedTask.artifacts().size());
554+
updatedTask.artifacts() != null ? updatedTask.artifacts().size() : 0);
554555
} else {
555556
LOGGER.warn("DefaultRequestHandler: Step 5 - Task {} not found in TaskStore!", taskId.get());
556557
}
@@ -611,7 +612,7 @@ public Flow.Publisher<StreamingEventKind> onMessageSendStream(
611612
// Store push notification config SYNCHRONOUSLY for new tasks before agent starts
612613
// This ensures config is available when MainEventBusProcessor sends push notifications
613614
// For existing tasks, config is stored in initMessageSend()
614-
if (mss.task() == null && shouldAddPushInfo(params)) {
615+
if (mss.task() == null && pushConfigStore != null && params.configuration() != null && params.configuration().pushNotificationConfig() != null) {
615616
// Satisfy Nullaway
616617
Objects.requireNonNull(taskId.get(), "taskId was null");
617618
LOGGER.debug("Storing push notification config for new streaming task {} EARLY (original taskId from params: {})",
@@ -849,10 +850,6 @@ public void onDeleteTaskPushNotificationConfig(
849850
pushConfigStore.deleteInfo(params.id(), params.pushNotificationConfigId());
850851
}
851852

852-
private boolean shouldAddPushInfo(MessageSendParams params) {
853-
return pushConfigStore != null && params.configuration() != null && params.configuration().pushNotificationConfig() != null;
854-
}
855-
856853
/**
857854
* Register and execute the agent asynchronously in the agent-executor thread pool.
858855
*
@@ -973,7 +970,7 @@ private MessageSendSetup initMessageSend(MessageSendParams params, ServerCallCon
973970
LOGGER.debug("Found task updating with message {}", params.message());
974971
task = taskManager.updateWithMessage(params.message(), task);
975972

976-
if (shouldAddPushInfo(params)) {
973+
if (pushConfigStore != null && params.configuration() != null && params.configuration().pushNotificationConfig() != null) {
977974
LOGGER.debug("Adding push info");
978975
pushConfigStore.setInfo(task.id(), params.configuration().pushNotificationConfig());
979976
}

server-common/src/main/java/io/a2a/server/tasks/BasePushNotificationSender.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public void sendNotification(StreamingEventKind event) {
7878
String nextPageToken = null;
7979
do {
8080
ListTaskPushNotificationConfigResult pageResult = configStore.getInfo(new ListTaskPushNotificationConfigParams(taskId,
81-
DEFAULT_PAGE_SIZE, nextPageToken, ""));
81+
DEFAULT_PAGE_SIZE, nextPageToken == null ? "" : nextPageToken, ""));
8282
if (!pageResult.configs().isEmpty()) {
8383
configs.addAll(pageResult.configs());
8484
}
@@ -111,11 +111,14 @@ public void sendNotification(StreamingEventKind event) {
111111
protected @Nullable String extractTaskId(StreamingEventKind event) {
112112
if (event instanceof Task task) {
113113
return task.id();
114-
} else if (event instanceof Message message) {
114+
}
115+
if (event instanceof Message message) {
115116
return message.taskId();
116-
} else if (event instanceof TaskStatusUpdateEvent statusUpdate) {
117+
}
118+
if (event instanceof TaskStatusUpdateEvent statusUpdate) {
117119
return statusUpdate.taskId();
118-
} else if (event instanceof TaskArtifactUpdateEvent artifactUpdate) {
120+
}
121+
if (event instanceof TaskArtifactUpdateEvent artifactUpdate) {
119122
return artifactUpdate.taskId();
120123
}
121124
throw new IllegalStateException("Unknown StreamingEventKind: " + event);

server-common/src/main/java/io/a2a/server/tasks/InMemoryPushNotificationConfigStore.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public PushNotificationConfig setInfo(String taskId, PushNotificationConfig noti
4141
Iterator<PushNotificationConfig> notificationConfigIterator = notificationConfigList.iterator();
4242
while (notificationConfigIterator.hasNext()) {
4343
PushNotificationConfig config = notificationConfigIterator.next();
44-
if (config.id().equals(notificationConfig.id())) {
44+
if (config.id() != null && config.id().equals(notificationConfig.id())) {
4545
notificationConfigIterator.remove();
4646
break;
4747
}

server-common/src/main/java/io/a2a/server/tasks/TaskUpdater.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,20 @@
1414
import io.a2a.spec.TaskState;
1515
import io.a2a.spec.TaskStatus;
1616
import io.a2a.spec.TaskStatusUpdateEvent;
17+
import io.a2a.util.Assert;
1718
import org.jspecify.annotations.Nullable;
1819

1920
public class TaskUpdater {
2021
private final EventQueue eventQueue;
21-
private final @Nullable String taskId;
22-
private final @Nullable String contextId;
22+
private final String taskId;
23+
private final String contextId;
2324
private final AtomicBoolean terminalStateReached = new AtomicBoolean(false);
2425
private final Object stateLock = new Object();
2526

2627
public TaskUpdater(RequestContext context, EventQueue eventQueue) {
2728
this.eventQueue = eventQueue;
28-
this.taskId = context.getTaskId();
29-
this.contextId = context.getContextId();
29+
this.taskId = Assert.checkNotNullParam("taskId",context.getTaskId());
30+
this.contextId = Assert.checkNotNullParam("contextId",context.getContextId());
3031
}
3132

3233
private void updateStatus(TaskState taskState) {

0 commit comments

Comments
 (0)