Skip to content

Commit 2f0013f

Browse files
committed
refactor!: centralize server validation
This fixes #1093
1 parent 479eaf8 commit 2f0013f

46 files changed

Lines changed: 4722 additions & 1197 deletions

File tree

Some content is hidden

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

‎common/src/main/java/org/a2aproject/sdk/util/Assert.java‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ public final class Assert {
88
private Assert() {
99
}
1010

11+
/**
12+
* Checks whether the value is {@code null}, empty, or blank.
13+
*
14+
* @param value the value to check
15+
* @return {@code true} if the value is {@code null}, empty, or blank
16+
*/
17+
public static boolean isNullOrEmpty(@Nullable String value) {
18+
return value == null || value.isBlank();
19+
}
20+
1121
/**
1222
* Check that the named parameter is not {@code null}. Use a standard exception message if it is.
1323
*

‎common/src/main/java/org/a2aproject/sdk/util/NotNull.java‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
import java.lang.annotation.Target;
88

99
@Retention(RetentionPolicy.CLASS)
10-
@Target({ ElementType.FIELD, ElementType.LOCAL_VARIABLE, ElementType.METHOD, ElementType.PARAMETER })
10+
@Target({ ElementType.FIELD, ElementType.LOCAL_VARIABLE, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE_USE })
1111
@Documented
1212
/** Marks an element as non-null for static analysis. */
1313
public @interface NotNull {
14-
}
14+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package org.a2aproject.sdk.util;
2+
3+
import static org.junit.jupiter.api.Assertions.assertFalse;
4+
import static org.junit.jupiter.api.Assertions.assertTrue;
5+
6+
import org.junit.jupiter.api.Test;
7+
8+
public class AssertTest {
9+
10+
@Test
11+
public void testIsNullOrEmptyForNull() {
12+
assertTrue(Assert.isNullOrEmpty(null));
13+
}
14+
15+
@Test
16+
public void testIsNullOrEmptyForEmptyString() {
17+
assertTrue(Assert.isNullOrEmpty(""));
18+
}
19+
20+
@Test
21+
public void testIsNullOrEmptyForOrdinaryString() {
22+
assertFalse(Assert.isNullOrEmpty("value"));
23+
}
24+
25+
@Test
26+
public void testIsNullOrEmptyForWhitespaceString() {
27+
assertTrue(Assert.isNullOrEmpty(" "));
28+
}
29+
}

‎compat-0.3/reference/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/server/apps/quarkus/A2AServerRoutes_v0_3.java‎

Lines changed: 16 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
import jakarta.inject.Inject;
1818
import jakarta.inject.Singleton;
1919

20-
import com.google.gson.JsonParser;
2120
import com.google.gson.JsonSyntaxException;
2221
import io.quarkus.security.Authenticated;
2322
import io.quarkus.security.ForbiddenException;
@@ -30,23 +29,18 @@
3029
import org.a2aproject.sdk.compat03.conversion.A2AProtocol_v0_3;
3130
import org.a2aproject.sdk.compat03.json.JsonProcessingException_v0_3;
3231
import org.a2aproject.sdk.compat03.json.JsonUtil_v0_3;
33-
import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3;
3432
import org.a2aproject.sdk.compat03.spec.CancelTaskRequest_v0_3;
3533
import org.a2aproject.sdk.compat03.spec.DeleteTaskPushNotificationConfigRequest_v0_3;
3634
import org.a2aproject.sdk.compat03.spec.GetAuthenticatedExtendedCardRequest_v0_3;
3735
import org.a2aproject.sdk.compat03.spec.GetTaskPushNotificationConfigRequest_v0_3;
3836
import org.a2aproject.sdk.compat03.spec.GetTaskRequest_v0_3;
3937
import org.a2aproject.sdk.compat03.spec.InternalError_v0_3;
40-
import org.a2aproject.sdk.compat03.spec.InvalidParamsError_v0_3;
41-
import org.a2aproject.sdk.compat03.spec.InvalidRequestError_v0_3;
4238
import org.a2aproject.sdk.compat03.spec.JSONParseError_v0_3;
4339
import org.a2aproject.sdk.compat03.spec.JSONRPCError_v0_3;
4440
import org.a2aproject.sdk.compat03.spec.JSONRPCErrorResponse_v0_3;
45-
import org.a2aproject.sdk.compat03.spec.JSONRPCMessage_v0_3;
4641
import org.a2aproject.sdk.compat03.spec.JSONRPCRequest_v0_3;
4742
import org.a2aproject.sdk.compat03.spec.JSONRPCResponse_v0_3;
4843
import org.a2aproject.sdk.compat03.spec.ListTaskPushNotificationConfigRequest_v0_3;
49-
import org.a2aproject.sdk.compat03.spec.MethodNotFoundError_v0_3;
5044
import org.a2aproject.sdk.compat03.spec.NonStreamingJSONRPCRequest_v0_3;
5145
import org.a2aproject.sdk.compat03.spec.SendMessageRequest_v0_3;
5246
import org.a2aproject.sdk.compat03.spec.SendStreamingMessageRequest_v0_3;
@@ -55,6 +49,7 @@
5549
import org.a2aproject.sdk.compat03.spec.TaskResubscriptionRequest_v0_3;
5650
import org.a2aproject.sdk.compat03.spec.UnsupportedOperationError_v0_3;
5751
import org.a2aproject.sdk.compat03.transport.jsonrpc.handler.JSONRPCHandler_v0_3;
52+
import org.a2aproject.sdk.compat03.transport.jsonrpc.handler.JSONRPCRequestDecoder_v0_3;
5853
import org.a2aproject.sdk.compat03.util.Utils_v0_3;
5954
import org.a2aproject.sdk.server.PublicAgentCard;
6055
import org.a2aproject.sdk.server.ServerCallContext;
@@ -131,56 +126,31 @@ private static boolean hasNonDefaultV10AgentCard() {
131126
}
132127

133128
@Authenticated
134-
public void invokeJSONRPCHandler(String body, RoutingContext rc) {
129+
public void invokeJSONRPCHandler(@Nullable String body, RoutingContext rc) {
135130
boolean streaming = false;
136131
ServerCallContext context = createCallContext(rc);
137132
JSONRPCResponse_v0_3<?> nonStreamingResponse = null;
138133
Multi<? extends JSONRPCResponse_v0_3<?>> streamingResponse = null;
139134
JSONRPCErrorResponse_v0_3 error = null;
140135
Object requestId = null;
141136
try {
142-
com.google.gson.JsonObject node;
143-
try {
144-
node = JsonParser.parseString(body).getAsJsonObject();
145-
} catch (Exception e) {
146-
throw new JSONParseError_v0_3(e.getMessage());
147-
}
148-
149-
// Extract id field early so error responses can include it
150-
com.google.gson.JsonElement idElement = node.get("id");
151-
if (idElement != null && !idElement.isJsonNull() && !idElement.isJsonPrimitive()) {
152-
throw new InvalidRequestError_v0_3("Invalid JSON-RPC request: 'id' must be a string, number, or null");
153-
}
154-
if (idElement != null && !idElement.isJsonNull() && idElement.isJsonPrimitive()) {
155-
com.google.gson.JsonPrimitive idPrimitive = idElement.getAsJsonPrimitive();
156-
requestId = idPrimitive.isNumber() ? idPrimitive.getAsLong() : idPrimitive.getAsString();
157-
}
158-
159-
// Validate jsonrpc field
160-
com.google.gson.JsonElement jsonrpcElement = node.get("jsonrpc");
161-
if (jsonrpcElement == null || !jsonrpcElement.isJsonPrimitive()
162-
|| !JSONRPCMessage_v0_3.JSONRPC_VERSION.equals(jsonrpcElement.getAsString())) {
163-
throw new InvalidRequestError_v0_3("Invalid JSON-RPC request: missing or invalid 'jsonrpc' field");
164-
}
165-
166-
// Validate method field
167-
com.google.gson.JsonElement methodElement = node.get("method");
168-
if (methodElement == null || !methodElement.isJsonPrimitive()) {
169-
throw new InvalidRequestError_v0_3("Invalid JSON-RPC request: missing or invalid 'method' field");
137+
JSONRPCRequestDecoder_v0_3.DecodeResult result = JSONRPCRequestDecoder_v0_3.decode(body);
138+
if (result instanceof JSONRPCRequestDecoder_v0_3.DecodeFailure failure) {
139+
requestId = failure.requestId();
140+
if (failure.methodName() != null) {
141+
context.getState().put(METHOD_NAME_KEY, failure.methodName());
142+
}
143+
throw failure.error();
170144
}
171-
172-
String methodName = methodElement.getAsString();
173-
context.getState().put(METHOD_NAME_KEY, methodName);
174-
175-
streaming = SendStreamingMessageRequest_v0_3.METHOD.equals(methodName)
176-
|| TaskResubscriptionRequest_v0_3.METHOD.equals(methodName);
177-
178-
if (streaming) {
179-
StreamingJSONRPCRequest_v0_3<?> request = deserializeStreamingRequest(body, methodName);
145+
JSONRPCRequestDecoder_v0_3.DecodedRequest decoded = (JSONRPCRequestDecoder_v0_3.DecodedRequest) result;
146+
requestId = decoded.requestId();
147+
context.getState().put(METHOD_NAME_KEY, decoded.methodName());
148+
if (decoded.request() instanceof StreamingJSONRPCRequest_v0_3<?> request) {
149+
streaming = true;
180150
streamingResponse = processStreamingRequest(request, context);
181151
} else {
182-
NonStreamingJSONRPCRequest_v0_3<?> request = deserializeNonStreamingRequest(body, methodName);
183-
nonStreamingResponse = processNonStreamingRequest(request, context);
152+
nonStreamingResponse = processNonStreamingRequest(
153+
(NonStreamingJSONRPCRequest_v0_3<?>) decoded.request(), context);
184154
}
185155
} catch (JSONRPCError_v0_3 e) {
186156
error = new JSONRPCErrorResponse_v0_3(requestId, e);
@@ -212,40 +182,6 @@ private static String formatSseEvent(Object data, long id) {
212182
return "data: " + Utils_v0_3.toJsonString(data) + "\nid: " + id + "\n\n";
213183
}
214184

215-
private NonStreamingJSONRPCRequest_v0_3<?> deserializeNonStreamingRequest(String body, String methodName) {
216-
try {
217-
return switch (methodName) {
218-
case GetTaskRequest_v0_3.METHOD -> JsonUtil_v0_3.fromJson(body, GetTaskRequest_v0_3.class);
219-
case CancelTaskRequest_v0_3.METHOD -> JsonUtil_v0_3.fromJson(body, CancelTaskRequest_v0_3.class);
220-
case SendMessageRequest_v0_3.METHOD -> JsonUtil_v0_3.fromJson(body, SendMessageRequest_v0_3.class);
221-
case SetTaskPushNotificationConfigRequest_v0_3.METHOD -> JsonUtil_v0_3.fromJson(body, SetTaskPushNotificationConfigRequest_v0_3.class);
222-
case GetTaskPushNotificationConfigRequest_v0_3.METHOD -> JsonUtil_v0_3.fromJson(body, GetTaskPushNotificationConfigRequest_v0_3.class);
223-
case ListTaskPushNotificationConfigRequest_v0_3.METHOD -> JsonUtil_v0_3.fromJson(body, ListTaskPushNotificationConfigRequest_v0_3.class);
224-
case DeleteTaskPushNotificationConfigRequest_v0_3.METHOD -> JsonUtil_v0_3.fromJson(body, DeleteTaskPushNotificationConfigRequest_v0_3.class);
225-
case GetAuthenticatedExtendedCardRequest_v0_3.METHOD -> JsonUtil_v0_3.fromJson(body, GetAuthenticatedExtendedCardRequest_v0_3.class);
226-
default -> throw new MethodNotFoundError_v0_3();
227-
};
228-
} catch (JSONRPCError_v0_3 e) {
229-
throw e;
230-
} catch (Exception e) {
231-
throw new InvalidParamsError_v0_3(e.getMessage());
232-
}
233-
}
234-
235-
private StreamingJSONRPCRequest_v0_3<?> deserializeStreamingRequest(String body, String methodName) {
236-
try {
237-
return switch (methodName) {
238-
case SendStreamingMessageRequest_v0_3.METHOD -> JsonUtil_v0_3.fromJson(body, SendStreamingMessageRequest_v0_3.class);
239-
case TaskResubscriptionRequest_v0_3.METHOD -> JsonUtil_v0_3.fromJson(body, TaskResubscriptionRequest_v0_3.class);
240-
default -> throw new MethodNotFoundError_v0_3();
241-
};
242-
} catch (JSONRPCError_v0_3 e) {
243-
throw e;
244-
} catch (Exception e) {
245-
throw new InvalidParamsError_v0_3(e.getMessage());
246-
}
247-
}
248-
249185
private JSONRPCResponse_v0_3<?> processNonStreamingRequest(
250186
NonStreamingJSONRPCRequest_v0_3<?> request, ServerCallContext context) {
251187
if (request instanceof GetTaskRequest_v0_3 req) {

0 commit comments

Comments
 (0)